1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Rally</title> 7 8 <!-- Theme Color --> 9 <meta name="theme-color" content="#000000"> 10 <meta name="msapplication-navbutton-color" content="#000000"> 11 <meta name="apple-mobile-web-app-status-bar-style" content="black"> 12 13 <!-- Favicon --> 14 <link rel="icon" type="image/png" href="/static/images/rally_favicon.png?v=2025"> 15 <link rel="apple-touch-icon" href="/static/img/apple-touch-icon-green-small.png?v=2025"> 16 17 <!-- Basic Meta Tags --> 18 <meta name="description" content="One app to run your season. Built by players, for players."> 19 <meta name="author" content="Rally"> 20 <meta name="robots" content="index, follow"> 21 22 <!-- Open Graph Meta Tags --> 23 <meta property="og:type" content="website"> 24 <meta property="og:title" content="Rally"> 25 <meta property="og:description" content="One app to run your season. Built by players, for players."> 26 <meta property="og:image" content="https://www.lovetorally.com/preview.png?v=20260719og"> 27 <meta property="og:image:secure_url" content="https://www.lovetorally.com/preview.png?v=20260719og"> 28 <meta property="og:image:alt" content="Rally - Platform Tennis Management"> 29 <meta property="og:image:type" content="image/png"> 30 <meta property="og:image:width" content="1200"> 31 <meta property="og:image:height" content="630"> 32 <meta property="og:url" content="http://www.lovetorally.com/login"> 33 <meta property="og:site_name" content="Rally"> 34 <meta property="og:locale" content="en_US"> 35 36 <!-- Twitter Card Meta Tags --> 37 <meta name="twitter:card" content="summary_large_image"> 38 <meta name="twitter:title" content="Rally"> 39 <meta name="twitter:description" content="One app to run your season. Built by players, for players."> 40 <meta name="twitter:image" content="https://www.lovetorally.com/preview.png?v=20260719og"> 41 <meta name="twitter:image:alt" content="Rally - Platform Tennis Management"> 42 <meta name="twitter:site" content="@rally"> 43 <meta name="twitter:creator" content="@rally"> 44 45 <!-- Additional Social Media Tags --> 46 <meta property="fb:app_id" content=""> 47 <meta name="application-name" content="Rally"> 48 <meta name="apple-mobile-web-app-title" content="Rally"> 49 <meta name="apple-mobile-web-app-capable" content="yes"> 50 <meta name="apple-mobile-web-app-status-bar-style" content="black"> 51 <meta name="format-detection" content="telephone=no"> 52 53 <!-- Canonical URL --> 54 <link rel="canonical" href="https://lovetorally.com/login"> 55 <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> 56 <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> 57 <link href="/static/css/styles.css" rel="stylesheet"> 58 <!-- Activity tracking --> 59
59<script src="/static/js/activity-tracker.js"></script>
59 60 <style> 61 :root { 62 --primary-color: #007417; /* Rally green */ 63 --text-color: #333; 64 --border-color: #dee2e6; 65 } 66 67 body { 68 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 69 background: linear-gradient(135deg, #085454, #0f172a); 70 display: flex; 71 justify-content: center; 72 align-items: center; 73 min-height: 100vh; 74 margin: 0; 75 } 76 77 .auth-container { 78 background: white; 79 border-radius: 12px; 80 box-shadow: 0 4px 6px rgba(0,0,0,0.1); 81 width: 100%; 82 max-width: 800px; 83 text-align: center; 84 position: static; 85 align-self: center; 86 } 87 88 .auth-content { 89 padding: 0 60px 60px 60px; 90 } 91 92 .logo-header { 93 background: #000; 94 border-radius: 12px 12px 0 0; 95 min-height: 120px; 96 display: flex; 97 flex-direction: column; 98 align-items: center; 99 justify-content: center; 100 text-align: center; 101 padding: 0; 102 } 103 104 .logo-img { 105 width: 256px; 106 height: auto; 107 margin-bottom: 0; 108 filter: none; 109 } 110 111 .form-control { 112 border-radius: 8px; 113 padding: 12px; 114 margin-bottom: 20px; 115 border: 1px solid var(--border-color); 116 font-size: 1.15rem; 117 } 118 119 .form-control:focus { 120 border-color: var(--primary-color); 121 box-shadow: 0 0 0 0.2rem rgba(0, 116, 23, 0.15); 122 } 123 124 .form-control:disabled { 125 background-color: #f8f9fa; 126 border-color: #e9ecef; 127 color: #6c757d; 128 cursor: not-allowed; 129 } 130 131 .form-control:disabled + label { 132 color: #adb5bd; 133 } 134 135 .btn-primary { 136 background-color: #085454 !important; 137 color: #fff !important; 138 border: none !important; 139 border-radius: 8px; 140 padding: 12px; 141 width: 100%; 142 font-weight: 500; 143 transition: all 0.3s ease; 144 font-size: 1.15rem; 145 position: relative; 146 overflow: hidden; 147 } 148 149 .btn-primary:hover:not(:disabled) { 150 background-color: #064040 !important; 151 transform: translateY(-1px); 152 box-shadow: 0 4px 8px rgba(0,0,0,0.2); 153 } 154 155 .btn-primary:disabled { 156 background-color: #6c757d !important; 157 cursor: not-allowed; 158 transform: none; 159 box-shadow: none; 160 } 161 162 .btn-primary:disabled:hover { 163 background-color: #6c757d !important; 164 transform: none; 165 } 166 167 /* Loading animation styles */ 168 .btn-loading { 169 position: relative; 170 color: transparent !important; 171 } 172 173 .btn-loading::after { 174 content: ''; 175 position: absolute; 176 width: 20px; 177 height: 20px; 178 top: 50%; 179 left: 50%; 180 margin-left: -10px; 181 margin-top: -10px; 182 border: 2px solid transparent; 183 border-top: 2px solid #ffffff; 184 border-radius: 50%; 185 animation: spin 1s linear infinite; 186 } 187 188 @keyframes spin { 189 0% { transform: rotate(0deg); } 190 100% { transform: rotate(360deg); } 191 } 192 193 /* Pulse animation for button press */ 194 .btn-pulse { 195 animation: pulse 0.3s ease-in-out; 196 } 197 198 @keyframes pulse { 199 0% { transform: scale(1); } 200 50% { transform: scale(0.98); } 201 100% { transform: scale(1); } 202 } 203 204 .error-message { 205 color: #dc3545; 206 margin-top: 10px; 207 display: none; 208 } 209 210 .auth-error-container { 211 margin-bottom: 20px; 212 padding: 15px; 213 border-radius: 8px; 214 background-color: #fff3f3; 215 border: 1px solid #ffcdd2; 216 display: none; 217 } 218 219 .auth-error-container.active { 220 display: block; 221 } 222 223 .auth-error-message { 224 color: #dc3545; 225 margin: 0; 226 font-size: 14px; 227 text-align: center; 228 } 229 230 .form-floating { 231 margin-bottom: 20px; 232 } 233 234 .form-floating label { 235 color: #6c757d; 236 } 237 238 .password-match { 239 border-color: #28a745 !important; 240 box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.15) !important; 241 } 242 243 .password-mismatch { 244 border-color: #dc3545 !important; 245 box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.15) !important; 246 } 247 248 .password-feedback { 249 font-size: 0.875rem; 250 margin-top: 5px; 251 margin-bottom: 15px; 252 } 253 254 .password-feedback.match { 255 color: #28a745; 256 } 257 258 .password-feedback.mismatch { 259 color: #dc3545; 260 } 261 262 .password-feedback i { 263 margin-right: 5px; 264 } 265 266 .auth-tabs { 267 display: flex; 268 margin-bottom: 30px; 269 border-bottom: 1px solid var(--border-color); 270 } 271 272 .auth-tab { 273 flex: 1; 274 padding: 15px; 275 cursor: pointer; 276 transition: all 0.3s ease; 277 } 278 279 .auth-tab.active { 280 border-bottom: 2px solid var(--primary-color); 281 color: var(--primary-color); 282 } 283 284 .auth-form { 285 display: none; 286 } 287 288 .auth-form.active { 289 display: block; 290 } 291 292 .form-row { 293 display: flex; 294 gap: 15px; 295 } 296 297 .form-row .form-floating { 298 flex: 1; 299 } 300 301 .auth-hero { display: none; } 302 .white-logo { filter: brightness(0) invert(1); } 303 304 .rally-footer { 305 position: fixed; 306 left: 0; 307 bottom: 0; 308 width: 100%; 309 background: #fff; 310 color: #888; 311 font-size: 11px; 312 text-align: center; 313 padding: 8px 0 6px 0; 314 box-shadow: 0 -2px 8px rgba(0,0,0,0.03); 315 z-index: 100; 316 line-height: 1.4; 317 } 318 319 .rally-footer a { 320 color: #6c757d; 321 text-decoration: none; 322 margin: 0 8px; 323 font-size: 10px; 324 } 325 326 .rally-footer a:hover { 327 color: #333; 328 text-decoration: underline; 329 } 330 331 .login-stack { 332 display: flex; 333 flex-direction: column; 334 align-items: center; 335 width: 100vw; 336 min-height: 100vh; 337 } 338 .auth-container { 339 position: static; 340 } 341 342 /* Rally logo color styling */ 343 .rally-logo { 344 transform: translateY(3px); 345 } 346 347 /* Forgot Password Link Styling */ 348 .forgot-password-link { 349 color: #6c757d; 350 text-decoration: none; 351 font-size: 0.9rem; 352 transition: color 0.2s ease; 353 } 354 355 .forgot-password-link:hover { 356 color: var(--primary-color); 357 text-decoration: underline; 358 } 359 360 </style> 361</head> 362<body> 363 <div class="login-stack"> 364 <div style="height:87.5px;"></div> 365 <div style="display:flex; justify-content:center; align-items:center;"> 366 <img src="/static/rallylogo.png" alt="Rally" class="logo-img rally-logo"> 367 </div> 368 <div style="height:87.5px;"></div> 369 370 371 <div class="auth-container"> 372 <div class="auth-content"> 373 <div class="auth-error-container" id="authErrorContainer"> 374 <p class="auth-error-message" id="authErrorMessage"></p> 375 </div> 376 377 <div class="auth-tabs"> 378 <div class="auth-tab active" onclick="showForm('login')">Sign In</div> 379 <div class="auth-tab " onclick="showForm('register')">Register</div> 380 </div> 381 382 <form id="loginForm" class="auth-form active"> 383 <div class="form-floating"> 384 <input type="email" class="form-control" id="loginEmail" placeholder="[email protected]" required> 385 <label for="loginEmail">Email address</label> 386 </div> 387 388 <div class="form-floating" style="position: relative;"> 389 <input type="password" class="form-control" id="loginPassword" placeholder="Password" required> 390 <label for="loginPassword">Password</label> 391 <button type="button" id="loginPasswordToggle" onclick="toggleLoginPassword()" style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #6c757d; font-size: 12px; cursor: pointer; z-index: 10;">SHOW</button> 392 </div> 393 394 <div class="error-message" id="loginErrorMessage"></div> 395 396 <button type="submit" class="btn btn-primary"> 397 <i class="fas fa-sign-in-alt me-2"></i>Let's go! 398 </button> 399 400 <!-- Separator Line --> 401 <div style="margin: 24px 0; text-align: center;"> 402 <div style="height: 1px; background-color: #e5e7eb; width: 100%;"></div> 403 </div> 404 405 <!-- Forgot Password and Support Chips --> 406 <div style="display: flex; flex-direction: row; justify-content: center; gap: 12px; margin-top: 20px;"> 407 <a href="/forgot-password" style=" 408 display: flex; 409 align-items: center; 410 justify-content: center; 411 width: 180px; 412 padding: 8px 12px; 413 border-radius: 25px; 414 background: #045454; 415 color: white; 416 text-decoration: none; 417 box-shadow: 0 4px 12px rgba(0,0,0,0.15); 418 font-size: 10px; 419 font-weight: 600; 420 transition: all 0.2s ease; 421 " onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'"> 422 <i class="fas fa-key" style="margin-right: 6px; font-size: 12px; color: white;"></i>
423 <span style="color: white; font-size: 10px; font-weight: 600;">Forgot Password?</span> 424 </a> 425 426 <a href="/mobile/support" style=" 427 display: flex; 428 align-items: center; 429 justify-content: center; 430 width: 180px; 431 padding: 8px 12px; 432 border-radius: 25px; 433 background: #045454; 434 color: white; 435 text-decoration: none; 436 box-shadow: 0 4px 12px rgba(0,0,0,0.15); 437 font-size: 10px; 438 font-weight: 600; 439 transition: all 0.2s ease; 440 " onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'"> 441 <i class="fas fa-question-circle" style="margin-right: 6px; font-size: 12px; color: white;"></i> 442 <span style="color: white; font-size: 10px; font-weight: 600;">Get support</span> 443 </a> 444 </div> 445 </form> 446 447 <form id="registerForm" class="auth-form "> 448 <!-- Account Information Section --> 449 <div class="form-section mb-4" style="background-color: #f8f9fa; border-radius: 8px; padding: 20px; border-left: 3px solid #007bff;"> 450 <h6 class="section-title mb-3" style="color: #495057; font-weight: 600; font-size: 0.9rem; margin-bottom: 1rem;">About you</h6> 451 452 <div class="form-row" style="margin-bottom: 0;"> 453 <div class="form-floating"> 454 <input type="text" class="form-control" id="firstName" placeholder="First Name" required> 455 <label for="firstName">First Name</label> 456 </div> 457 458 <div class="form-floating"> 459 <input type="text" class="form-control" id="lastName" placeholder="Last Name" required> 460 <label for="lastName">Last Name</label> 461 </div> 462 </div> 463 464 <div class="form-floating mb-3"> 465 <input type="email" class="form-control" id="registerEmail" placeholder="[email protected]" required> 466 <label for="registerEmail">Email address</label> 467 </div> 468 469 <div class="form-floating mb-3"> 470 <input type="tel" class="form-control" id="phoneNumber" placeholder="(555) 123-4567" required> 471 <label for="phoneNumber">Phone Number</label> 472 </div> 473 474 <div class="form-floating mb-3" style="position: relative;"> 475 <input type="password" class="form-control" id="registerPassword" placeholder="Password" required> 476 <label for="registerPassword">Password</label> 477 <button type="button" id="registerPasswordToggle" onclick="toggleRegisterPassword()" style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #6c757d; font-size: 12px; cursor: pointer; z-index: 10;">SHOW</button> 478 </div> 479 480 <div class="form-floating mb-3" style="position: relative;"> 481 <input type="password" class="form-control" id="confirmPassword" placeholder="Confirm Password" required> 482 <label for="confirmPassword">Confirm Password</label> 483 <button type="button" id="confirmPasswordToggle" onclick="toggleConfirmPassword()" style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #6c757d; font-size: 12px; cursor: pointer; z-index: 10;">SHOW</button> 484 </div> 485 486 <div class="password-feedback" id="passwordFeedback" style="display: none;"> 487 <i class="fas fa-check-circle"></i>
488 <span id="passwordFeedbackText"></span> 489 </div> 490 </div> 491 492 <!-- Organization Settings Section --> 493 <div class="form-section mb-4" style="background-color: #f8f9fa; border-radius: 8px; padding: 20px; border-left: 3px solid #28a745;"> 494 <h6 class="section-title mb-3" style="color: #495057; font-weight: 600; font-size: 0.9rem; margin-bottom: 1rem;">Where you play</h6> 495 496 <div class="form-floating mb-3"> 497 <select class="form-control" id="league" required> 498 <option value="">Select your league</option> 499 </select> 500 <label for="league">League</label> 501 </div> 502 503 <div class="form-floating mb-3"> 504 <select class="form-control" id="club" required> 505 <option value="">Select your club</option> 506 </select> 507 <label for="club">Club</label> 508 </div> 509 510 <div class="form-floating mb-3"> 511 <select class="form-control" id="series" required> 512 <option value="">Select your series</option> 513 </select> 514 <label for="series">Series/Division</label> 515 </div> 516 </div> 517 518 <!-- Player Preferences Section --> 519 <div class="form-section mb-4" style="background-color: #f8f9fa; border-radius: 8px; padding: 20px; border-left: 3px solid #ffc107;"> 520 <h6 class="section-title mb-3" style="color: #495057; font-weight: 600; font-size: 0.9rem; margin-bottom: 1rem;">How you play</h6> 521 522 <div class="form-floating mb-3"> 523 <select class="form-control" id="adDeuce" required> 524 <option value="">Select preference...</option> 525 <option value="Ad">Ad</option> 526 <option value="Deuce">Deuce</option> 527 <option value="Either">Either</option> 528 </select> 529 <label for="adDeuce">Ad/Deuce Preference</label> 530 </div> 531 532 <div class="form-floating mb-3"> 533 <select class="form-control" id="dominantHand" required> 534 <option value="">Select hand...</option> 535 <option value="Righty">Righty</option> 536 <option value="Lefty">Lefty</option> 537 </select> 538 <label for="dominantHand">Dominant Hand</label> 539 </div> 540 </div> 541 542 <div class="form-check mb-3" style="text-align: left;"> 543 <input class="form-check-input" type="checkbox" id="textNotifications" required> 544 <label class="form-check-label" for="textNotifications" style="font-size: 0.85rem;"> 545 I agree to receive text message notifications from my team captain through Rally. Reply STOP to unsubscribe. 546 </label> 547 </div> 548 549 <!-- Apple App Store Guideline 1.2 / 5.1.4 / COPPA: 550 age >= 13 attestation must be a separate, explicit checkbox. --> 551 <div class="form-check mb-3" style="text-align: left;"> 552 <input class="form-check-input" type="checkbox" id="ageConfirmed" required> 553 <label class="form-check-label" for="ageConfirmed" style="font-size: 0.85rem;"> 554 I confirm I am at least 13 years old. 555 </label> 556 </div> 557 558 <!-- Apple App Store Guideline 1.2 / 5.1.1: explicit Terms acceptance at signup. --> 559 <div class="form-check mb-4" style="text-align: left;"> 560 <input class="form-check-input" type="checkbox" id="termsAccepted" required> 561 <label class="form-check-label" for="termsAccepted" style="font-size: 0.85rem;"> 562 I agree to the 563 <a href="/terms" target="_blank" rel="noopener">Terms of Use</a> 564 and 565 <a href="/privacy" target="_blank" rel="noopener">Privacy Policy</a>. 566 </label> 567 </div> 568 569 <div class="error-message" id="registerErrorMessage"></div> 570 571 <button type="submit" class="btn btn-primary"> 572 <i class="fas fa-user-plus me-2"></i>Let's go! 573 </button> 574 </form> 575 </div> 576 </div> 577 578 <!-- Extra spacing for auto-scroll functionality --> 579 <div style="height: 200px;"></div> 580 </div> 581 582
582<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
582 583
583<script> 584 // Button state management functions 585 function setButtonLoading(button, isLoading) { 586 if (isLoading) { 587 button.disabled = true; 588 button.classList.add('btn-loading'); 589 button.classList.add('btn-pulse'); 590 } else { 591 button.disabled = false; 592 button.classList.remove('btn-loading'); 593 button.classList.remove('btn-pulse'); 594 } 595 } 596 597 function resetButtonState(button) { 598 button.disabled = false; 599 button.classList.remove('btn-loading'); 600 button.classList.remove('btn-pulse'); 601 } 602 603 // Load leagues, clubs and series on page load with filtering 604 document.addEventListener('DOMContentLoaded', async () => { 605 // Set the correct tab based on the current URL or server-provided default 606 const currentPath = window.location.pathname; 607 const currentHash = window.location.hash; 608 let activeTab = 'login'; 609 610 // Override with URL if user directly navigated to a specific page 611 if (currentPath === '/register') { 612 activeTab = 'register'; 613 } else if (currentPath === '/login') { 614 activeTab = 'login'; 615 } 616 617 // Override with hash if present (e.g., /login#register) 618 if (currentHash === '#register') { 619 activeTab = 'register'; 620 } else if (currentHash === '#login') { 621 activeTab = 'login'; 622 } 623 624 // Ensure the correct tab is displayed 625 showForm(activeTab); 626 627 // Handle browser back/forward navigation 628 window.addEventListener('popstate', function(event) { 629 const currentPath = window.location.pathname; 630 if (currentPath === '/register') { 631 showForm('register'); 632 } else if (currentPath === '/login') { 633 showForm('login'); 634 } 635 }); 636 637 try { 638 await loadLeaguesAndFilters(); 639 } catch (error) { 640 console.error('Error loading registration data:', error); 641 // Retry after a short delay in case of temporary network issues 642 setTimeout(async () => { 643 try { 644 await loadLeaguesAndFilters(); 645 } catch (retryError) { 646 console.error('Retry failed:', retryError); 647 } 648 }, 2000); 649 } 650 }); 651 652 // Load leagues first, then set up filtering 653 async function loadLeaguesAndFilters() { 654 try { 655 // Load leagues first 656 const leaguesResponse = await fetch('/api/get-leagues'); 657 if (!leaguesResponse.ok) { 658 throw new Error('Failed to load leagues'); 659 } 660 661 const leagues = await leaguesResponse.json(); 662 const leagueSelect = document.getElementById('league'); 663 664 leagueSelect.innerHTML = '<option value="">Select your league</option>'; 665 666 if (leagues.leagues && Array.isArray(leagues.leagues)) {
667 leagues.leagues.forEach(league => { 668 const option = document.createElement('option'); 669 option.value = league.league_id; 670 option.textContent = league.league_name; 671 leagueSelect.appendChild(option); 672 }); 673 } 674 675 // Add event listener for league changes 676 leagueSelect.addEventListener('change', function() { 677 const selectedLeague = this.value; 678 const seriesSelect = document.getElementById('series'); 679 680 if (selectedLeague) { 681 loadClubsByLeague(selectedLeague); 682 } else { 683 loadClubsByLeague(); 684 } 685 686 // Series options depend on club; reset until a club is selected 687 seriesSelect.disabled = true; 688 seriesSelect.innerHTML = '<option value="">Select your club first</option>'; 689 690 // Clear club and series selections when league changes 691 document.getElementById('club').value = ''; 692 document.getElementById('series').value = ''; 693 }); 694 695 // Reload series when club changes so users only see series their club fields 696 document.getElementById('club').addEventListener('change', function() { 697 const selectedLeague = document.getElementById('league').value; 698 const selectedClub = this.value; 699 const seriesSelect = document.getElementById('series'); 700 701 if (selectedLeague && selectedClub) { 702 seriesSelect.disabled = false; 703 loadSeriesByLeague(selectedLeague, selectedClub); 704 } else { 705 seriesSelect.disabled = true; 706 seriesSelect.innerHTML = '<option value="">Select your club first</option>'; 707 } 708 709 document.getElementById('series').value = ''; 710 }); 711 712 // Load all clubs initially, but leave series empty until league is selected 713 loadClubsByLeague(); 714 715 // Initialize series dropdown as disabled until club is selected 716 const seriesSelect = document.getElementById('series'); 717 seriesSelect.disabled = true; 718 seriesSelect.innerHTML = '<option value="">Select your club first</option>'; 719 720 } catch (error) { 721 console.error('Error loading leagues:', error); 722 // Fallback to loading all clubs without league filtering 723 loadClubsByLeague(); 724 } 725 } 726 727 // Load clubs filtered by league 728 function loadClubsByLeague(leagueId = null) { 729 const url = leagueId ? `/api/get-clubs-by-league?league_id=${leagueId}` : '/api/get-clubs-by-league'; 730 731 fetch(url).then(r => r.json()).then(data => { 732 const clubSelect = document.getElementById('club'); 733 clubSelect.innerHTML = '<option value="">Select your club</option>'; 734 735 if (data.clubs && Array.isArray(data.clubs)) { 736 data.clubs.forEach(club => { 737 const option = document.createElement('option'); 738 option.value = club; 739 option.textContent = club; 740 clubSelect.appendChild(option); 741 }); 742 } 743 }).catch(error => { 744 console.error('Error loading clubs:', error); 745 // Fallback to all clubs 746 fetch('/api/get-clubs').then(r => r.json()).then(data => { 747 const clubSelect = document.getElementById('club'); 748 clubSelect.innerHTML = '<option value="">Select your club</option>'; 749 if (data.clubs && Array.isArray(data.clubs)) {
750 data.clubs.forEach(club => { 751 const option = document.createElement('option'); 752 option.value = club; 753 option.textContent = club; 754 clubSelect.appendChild(option); 755 }); 756 } 757 }); 758 }); 759 } 760 761 // Load series filtered by league and optionally by club (registration UX) 762 function loadSeriesByLeague(leagueId = null, clubName = null) { 763 // â ID-BASED: Use the enhanced API endpoint that returns series objects with IDs 764 let url = leagueId ? `/api/get-user-facing-series-by-league?league_id=${encodeURIComponent(leagueId)}` : '/api/get-user-facing-series-by-league'; 765 if (clubName) { 766 url += `${url.includes('?') ? '&' : '?'}club_name=${encodeURIComponent(clubName)}`; 767 } 768 769 console.log('ð Loading ID-based series for league:', leagueId, 'club:', clubName || '(all)'); 770 771 fetch(url).then(r => r.json()).then(data => { 772 const seriesSelect = document.getElementById('series'); 773 seriesSelect.innerHTML = '<option value="">Select your series</option>'; 774 775 if (data.series && Array.isArray(data.series)) { 776 console.log('ð Received series data:', data.series); 777 778 data.series.forEach(seriesObj => { 779 const option = document.createElement('option'); 780 781 // â DEFENSIVE: Check if we have the new ID-based format 782 if (typeof seriesObj === 'object' && seriesObj !== null && seriesObj.id) { 783 // â NEW: ID-based format with series objects 784 const displayName = seriesObj.display || seriesObj.name || `Series ${seriesObj.id}`; 785 786 option.value = JSON.stringify({ 787 id: seriesObj.id, 788 name: seriesObj.name || '', 789 display: displayName 790 }); 791 option.textContent = displayName; // Show user-friendly name 792 793 // Add data attributes for debugging 794 option.setAttribute('data-series-id', seriesObj.id); 795 option.setAttribute('data-series-name', seriesObj.name || ''); 796 option.setAttribute('data-series-display', displayName); 797 798 console.log(` â ID-based series: ${displayName} (ID: ${seriesObj.id})`); 799 } else if (typeof seriesObj === 'string') { 800 // ð LEGACY: String format (backward compatibility) 801 option.value = seriesObj; 802 option.textContent = seriesObj; 803 console.log(` ð Legacy series: ${seriesObj}`); 804 } else { 805 // ð¨ FALLBACK: Handle unexpected format 806 console.error(' â Unexpected series format:', seriesObj); 807 const fallbackText = String(seriesObj) || 'Unknown Series'; 808 option.value = fallbackText; 809 option.textContent = fallbackText; 810 } 811 812 seriesSelect.appendChild(option); 813 }); 814 815 console.log(`â Populated series dropdown with ${data.series.length} options`); 816 } 817 }).catch(error => { 818 console.error('â Error loading ID-based series:', error); 819 // Fallback to original API if new one fails 820 const fallbackUrl = leagueId ? `/api/get-series-by-league?league_id=${leagueId}` : '/api/get-series-by-league'; 821 console.log('ð Falling back to legacy series API'); 822 823 fetch(fallbackUrl).then(r => r.json()).then(data => { 824 const seriesSelect = document.getElementById('series'); 825 seriesSelect.innerHTML = '<option value="">Select your series</option>'; 826 if (data.series && Array.isArray(data.series)) {
827 data.series.forEach(series => { 828 const option = document.createElement('option'); 829 option.value = series; 830 option.textContent = series; 831 seriesSelect.appendChild(option); 832 }); 833 console.log(`ð Fallback: Populated series dropdown with ${data.series.length} legacy options`); 834 } 835 }).catch(fallbackError => { 836 console.error('â Fallback series loading also failed:', fallbackError); 837 838 // ð EMERGENCY FALLBACK: Hard-coded APTA Chicago series for immediate fix 839 if (leagueId === 'APTA_CHICAGO') { 840 console.log('ð Using emergency hard-coded APTA Chicago series'); 841 const seriesSelect = document.getElementById('series'); 842 seriesSelect.innerHTML = '<option value="">Select your series</option>'; 843 844 const emergencySeries = [ 845 'Series 1', 'Series 2', 'Series 3', 'Series 4', 'Series 5', 846 'Series 6', 'Series 7', 'Series 8', 'Series 9', 'Series 10', 847 'Series 11', 'Series 12', 'Series 13', 'Series 14', 'Series 15', 848 'Series 16', 'Series 17', 'Series 18', 'Series 19', 'Series 20' 849 ]; 850 851 emergencySeries.forEach(series => { 852 const option = document.createElement('option'); 853 option.value = series; 854 option.textContent = series; 855 seriesSelect.appendChild(option); 856 }); 857 858 console.log('ð Emergency fallback populated with 20 APTA Chicago series'); 859 } 860 }); 861 }); 862 } 863 864 // Password validation functions 865 function validatePasswordMatch() { 866 const password = document.getElementById('registerPassword').value; 867 const confirmPassword = document.getElementById('confirmPassword').value; 868 const passwordField = document.getElementById('registerPassword'); 869 const confirmField = document.getElementById('confirmPassword'); 870 const feedback = document.getElementById('passwordFeedback'); 871 const feedbackText = document.getElementById('passwordFeedbackText'); 872 const feedbackIcon = feedback.querySelector('i'); 873 874 // Only show feedback if both fields have content 875 if (password.length === 0 && confirmPassword.length === 0) { 876 feedback.style.display = 'none'; 877 passwordField.classList.remove('password-match', 'password-mismatch'); 878 confirmField.classList.remove('password-match', 'password-mismatch'); 879 return true; 880 } 881 882 // Only validate if confirm password has been touched 883 if (confirmPassword.length === 0) { 884 feedback.style.display = 'none'; 885 passwordField.classList.remove('password-match', 'password-mismatch'); 886 confirmField.classList.remove('password-match', 'password-mismatch'); 887 return true; 888 } 889 890 feedback.style.display = 'block'; 891 892 if (password === confirmPassword && password.length > 0) { 893 // Passwords match 894 passwordField.classList.remove('password-mismatch'); 895 passwordField.classList.add('password-match'); 896 confirmField.classList.remove('password-mismatch'); 897 confirmField.classList.add('password-match'); 898 899 feedback.classList.remove('mismatch'); 900 feedback.classList.add('match'); 901 feedbackIcon.className = 'fas fa-check-circle'; 902 feedbackText.textContent = 'Passwords match'; 903 return true; 904 } else { 905 // Passwords don't match 906 passwordField.classList.remove('password-match'); 907 passwordField.classList.add('password-mismatch'); 908 confirmField.classList.remove('password-match'); 909 confirmField.classList.add('password-mismatch'); 910 911 feedback.classList.remove('match'); 912 feedback.classList.add('mismatch'); 913 feedbackIcon.className = 'fas fa-times-circle'; 914 feedbackText.textContent = 'Passwords do not match'; 915 return false;
916 } 917 } 918 919 // Add event listeners for real-time password validation 920 document.addEventListener('DOMContentLoaded', function() { 921 const registerPassword = document.getElementById('registerPassword'); 922 const confirmPassword = document.getElementById('confirmPassword'); 923 924 if (registerPassword && confirmPassword) { 925 registerPassword.addEventListener('input', validatePasswordMatch); 926 confirmPassword.addEventListener('input', validatePasswordMatch); 927 928 // Also validate on blur events 929 registerPassword.addEventListener('blur', validatePasswordMatch); 930 confirmPassword.addEventListener('blur', validatePasswordMatch); 931 } 932 933 // Auto-scroll to bottom when text notifications checkbox is clicked 934 const textNotificationsCheckbox = document.getElementById('textNotifications'); 935 if (textNotificationsCheckbox) { 936 textNotificationsCheckbox.addEventListener('change', function() { 937 if (this.checked) { 938 // Auto-scroll down by 100px when checkbox is checked 939 setTimeout(() => { 940 window.scrollBy({ 941 top: 100, 942 behavior: 'smooth' 943 }); 944 }, 100); // Small delay to ensure checkbox state is updated 945 } 946 }); 947 } 948 }); 949 950 // Function to smoothly scroll to the top of the page 951 function scrollToTop() { 952 window.scrollTo({ 953 top: 0, 954 behavior: 'smooth' 955 }); 956 } 957 958 function showForm(formType) { 959 document.querySelectorAll('.auth-tab').forEach(tab => { 960 tab.classList.remove('active'); 961 }); 962 document.querySelectorAll('.auth-form').forEach(form => { 963 form.classList.remove('active'); 964 }); 965 966 document.querySelector(`.auth-tab:nth-child(${formType === 'login' ? 1 : 2})`).classList.add('active'); 967 document.getElementById(`${formType}Form`).classList.add('active'); 968 969 // Reset button states when switching forms 970 const loginButton = document.querySelector('#loginForm button[type="submit"]'); 971 const registerButton = document.querySelector('#registerForm button[type="submit"]'); 972 if (loginButton) resetButtonState(loginButton); 973 if (registerButton) resetButtonState(registerButton); 974 975 // Update URL to match the current tab (without page reload) 976 const newPath = formType === 'register' ? '/register' : '/login'; 977 if (window.location.pathname !== newPath) { 978 window.history.pushState(null, '', newPath); 979 } 980 981 // If switching to register form, ensure dropdowns are properly initialized 982 if (formType === 'register') { 983 setTimeout(() => { 984 // Only reload if leagues haven't been loaded yet 985 const leagueSelect = document.getElementById('league'); 986 if (leagueSelect.options.length <= 1) { 987 loadLeaguesAndFilters(); 988 } else { 989 // Ensure series dropdown is disabled if no league is selected 990 const seriesSelect = document.getElementById('series'); 991 if (!leagueSelect.value) { 992 seriesSelect.disabled = true; 993 seriesSelect.innerHTML = '<option value="">Select your series</option>'; 994 } 995 } 996 }, 100); 997 } 998 } 999 1000 document.getElementById('loginForm').addEventListener('submit', async function(e) { 1001 e.preventDefault(); 1002 1003 const loginButton = this.querySelector('button[type="submit"]'); 1004 setButtonLoading(loginButton, true); 1005 1006 const email = document.getElementById('loginEmail').value; 1007 const password = document.getElementById('loginPassword').value; 1008 const errorMessage = document.getElementById('loginErrorMessage'); 1009 const authErrorContainer = document.getElementById('authErrorContainer'); 1010 const authErrorMessage = document.getElementById('authErrorMessage'); 1011 1012 try { 1013 const response = await fetch('/api/login', { 1014 method: 'POST', 1015 headers: { 1016 'Content-Type': 'application/json' 1017 }, 1018 credentials: 'include', 1019 body: JSON.stringify({ email, password }) 1020 }); 1021 1022 const data = await response.json(); 1023 if (response.ok && data.status === 'success') { 1024 if (data.redirect) { 1025 window.location.href = data.redirect; 1026 } else { 1027 window.location.href = '/'; 1028 } 1029 } else { 1030 resetButtonState(loginButton); 1031 if (response.status === 401) { 1032 authErrorMessage.innerHTML = 'Your email or password is incorrect. If you forgot your password, no worries ⦠click <a href="/forgot-password">here</a> to reset it. Or click <a href="/mobile/support">here</a> for support.'; 1033 } else { 1034 authErrorMessage.textContent = data.error || 'An error occurred. Please try again.'; 1035 } 1036 authErrorContainer.classList.add('active'); 1037 errorMessage.style.display = 'none'; 1038 scrollToTop(); 1039 } 1040 } catch (error) { 1041 resetButtonState(loginButton); 1042 authErrorMessage.textContent = 'An error occurred. Please try again.'; 1043 authErrorContainer.classList.add('active'); 1044 errorMessage.style.display = 'none'; 1045 scrollToTop(); 1046 } 1047 }); 1048 1049 document.getElementById('registerForm').addEventListener('submit', async function(e) { 1050 e.preventDefault(); 1051 1052 const registerButton = this.querySelector('button[type="submit"]'); 1053 setButtonLoading(registerButton, true); 1054 1055 const email = document.getElementById('registerEmail').value; 1056 let phoneNumber = document.getElementById('phoneNumber').value.trim(); 1057 1058 // â PHONE NORMALIZATION: Use centralized phone validation 1059 if (phoneNumber) { 1060 const phoneResult = normalizePhoneNumber(phoneNumber); 1061 if (!phoneResult.success) { 1062 errorMessage.textContent = phoneResult.error; 1063 errorMessage.style.display = 'block'; 1064 setButtonLoading(registerButton, false); 1065 return; 1066 } 1067 phoneNumber = phoneResult.normalized; 1068 } 1069 1070 const password = document.getElementById('registerPassword').value; 1071 const confirmPassword = document.getElementById('confirmPassword').value; 1072 const firstName = document.getElementById('firstName').value; 1073 const lastName = document.getElementById('lastName').value; 1074 const league = document.getElementById('league').value; 1075 const club = document.getElementById('club').value; 1076 const series = document.getElementById('series').value; 1077 const adDeuce = document.getElementById('adDeuce').value; 1078 const dominantHand = document.getElementById('dominantHand').value; 1079 const errorMessage = document.getElementById('registerErrorMessage'); 1080 const authErrorContainer = document.getElementById('authErrorContainer'); 1081 const authErrorMessage = document.getElementById('authErrorMessage'); 1082 1083 // Validate password matching before submission 1084 if (!validatePasswordMatch()) { 1085 resetButtonState(registerButton); 1086 authErrorMessage.textContent = 'Passwords do not match. Please check your password entries.'; 1087 authErrorContainer.classList.add('active'); 1088 scrollToTop(); 1089 return; 1090 } 1091 1092 // Validate that passwords match 1093 if (password !== confirmPassword) { 1094 resetButtonState(registerButton); 1095 authErrorMessage.textContent = 'Passwords do not match'; 1096 authErrorContainer.classList.add('active'); 1097 scrollToTop(); 1098 return; 1099 } 1100 1101 // Validate that league is selected 1102 if (!league) { 1103 resetButtonState(registerButton); 1104 authErrorMessage.textContent = 'Please select a league'; 1105 authErrorContainer.classList.add('active'); 1106 scrollToTop(); 1107 return; 1108 } 1109 1110 try { 1111 // â ID-BASED: Enhanced debug logging and series data extraction 1112 console.log('ð ID-BASED Registration Debug - Form Values:'); 1113 console.log(' Email:', email); 1114 console.log(' First Name:', firstName); 1115 console.log(' Last Name:', lastName); 1116 console.log(' League:', league); 1117 console.log(' Club:', club); 1118 console.log(' Series (raw):', series); 1119 1120 // â ID-BASED: Extract series data from dropdown 1121 const seriesSelect = document.getElementById('series'); 1122 const selectedOption = seriesSelect.options[seriesSelect.selectedIndex]; 1123 let seriesId = null; 1124 let seriesDisplay = null; 1125 1126 // Check if we have ID-based series data 1127 if (selectedOption && selectedOption.value) { 1128 try { 1129 // Try to parse as JSON (new ID-based format) 1130 const seriesData = JSON.parse(selectedOption.value); 1131 if (seriesData.id) { 1132 seriesId = seriesData.id; 1133 seriesDisplay = seriesData.display || selectedOption.textContent; 1134 console.log(' â ID-BASED Series Data:', seriesData); 1135 } 1136 } catch (e) {
1137 // Fallback: treat as series name (backward compatibility) 1138 seriesDisplay = selectedOption.value; 1139 console.log(' ð LEGACY Series Name:', seriesDisplay); 1140 } 1141 } 1142 1143 console.log(' Series ID:', seriesId); 1144 console.log(' Series Display:', seriesDisplay); 1145 console.log(' Series Value (raw):', selectedOption?.value); 1146 1147 // Apple Guideline 1.2 / 5.1.4 / COPPA: pull explicit attestations. 1148 const termsAccepted = document.getElementById('termsAccepted')?.checked === true; 1149 const ageConfirmed = document.getElementById('ageConfirmed')?.checked === true; 1150 1151 // â ID-BASED: Build registration payload with series_id priority 1152 const registrationPayload = { 1153 email, 1154 phoneNumber, 1155 password, 1156 firstName, 1157 lastName, 1158 league, 1159 club, 1160 adDeuce, 1161 dominantHand, 1162 terms_accepted: termsAccepted, 1163 terms_version: '2026.06', 1164 age_at_least_13: ageConfirmed 1165 }; 1166 1167 // Add series data based on what we extracted 1168 if (seriesId) { 1169 // NEW: ID-based registration (preferred) 1170 registrationPayload.series_id = seriesId; 1171 registrationPayload.series_display = seriesDisplay; 1172 console.log(' ð¤ Sending ID-based registration with series_id:', seriesId); 1173 } else { 1174 // FALLBACK: Name-based registration (backward compatibility) 1175 registrationPayload.series = series; 1176 console.log(' ð¤ Sending legacy registration with series name:', series); 1177 } 1178 1179 const response = await fetch('/api/register', { 1180 method: 'POST', 1181 headers: { 1182 'Content-Type': 'application/json' 1183 }, 1184 credentials: 'include', 1185 body: JSON.stringify(registrationPayload) 1186 }); 1187 1188 const data = await response.json(); 1189 1190 // Debug logging for the API response 1191 console.log('ð Registration API Response:'); 1192 console.log(' Status Code:', response.status); 1193 console.log(' Response Data:', data); 1194 1195 if (response.ok && data.status === 'success') { 1196 console.log('â Registration successful, redirecting...'); 1197 if (data.redirect) { 1198 window.location.href = data.redirect; 1199 } else { 1200 window.location.href = '/'; 1201 } 1202 authErrorContainer.classList.remove('active'); 1203 errorMessage.style.display = 'none'; 1204 } else { 1205 console.log('â Registration failed:', data.error); 1206 resetButtonState(registerButton); 1207 const errorText = data.error || 'Registration failed'; 1208 // If error mentions support, add hyperlink 1209 if (errorText.includes('support') || errorText.
1209includes('Support')) { 1210 // Replace "Please contact support for assistance" with a hyperlink 1211 authErrorMessage.innerHTML = errorText.replace( 1212 /(Please contact support for assistance)/gi, 1213 'Please <a href="/mobile/support">contact support</a> for assistance' 1214 ); 1215 } else { 1216 authErrorMessage.textContent = errorText; 1217 } 1218 authErrorContainer.classList.add('active'); 1219 errorMessage.style.display = 'none'; 1220 scrollToTop(); 1221 } 1222 } catch (error) { 1223 resetButtonState(registerButton); 1224 authErrorMessage.textContent = 'An error occurred. Please try again.'; 1225 authErrorContainer.classList.add('active'); 1226 errorMessage.style.display = 'none'; 1227 scrollToTop(); 1228 } 1229 }); 1230 1231 // Auto-scroll functionality for registration form 1232 function addAutoScrollOnSelect(selectId, scrollAmount) { 1233 var el = document.getElementById(selectId); 1234 if (el) { 1235 el.addEventListener('change', function() { 1236 window.scrollBy({ top: scrollAmount, behavior: 'smooth' }); 1237 }); 1238 } 1239 } 1240 1241 // Auto-scroll for specific fields 1242 document.addEventListener('DOMContentLoaded', function() { 1243 // Add auto-scroll for registration form fields 1244 addAutoScrollOnSelect('confirmPassword', 100); 1245 addAutoScrollOnSelect('league', 100); 1246 addAutoScrollOnSelect('club', 100); 1247 addAutoScrollOnSelect('series', 100); 1248 addAutoScrollOnSelect('adDeuce', 100); 1249 addAutoScrollOnSelect('dominantHand', 100); 1250 }); 1251 1252 // Password visibility toggle functions 1253 function toggleLoginPassword() { 1254 const passwordField = document.getElementById('loginPassword'); 1255 const toggleButton = document.getElementById('loginPasswordToggle'); 1256 1257 if (passwordField.type === 'password') { 1258 passwordField.type = 'text'; 1259 toggleButton.textContent = 'HIDE'; 1260 } else { 1261 passwordField.type = 'password'; 1262 toggleButton.textContent = 'SHOW'; 1263 } 1264 } 1265 1266 function toggleRegisterPassword() { 1267 const passwordField = document.getElementById('registerPassword'); 1268 const toggleButton = document.getElementById('registerPasswordToggle'); 1269 1270 if (passwordField.type === 'password') { 1271 passwordField.type = 'text'; 1272 toggleButton.textContent = 'HIDE'; 1273 } else { 1274 passwordField.type = 'password'; 1275 toggleButton.textContent = 'SHOW'; 1276 } 1277 } 1278 1279 function toggleConfirmPassword() { 1280 const passwordField = document.getElementById('confirmPassword'); 1281 const toggleButton = document.getElementById('confirmPasswordToggle'); 1282 1283 if (passwordField.type === 'password') { 1284 passwordField.type = 'text'; 1285 toggleButton.textContent = 'HIDE'; 1286 } else { 1287 passwordField.type = 'password'; 1288 toggleButton.textContent = 'SHOW'; 1289 } 1290 } 1291 </script>
1291 1292 1293 <!-- Phone Validation Script --> 1294
1294<script src="/static/js/phone-validation.js"></script>
1294 1295 1296 <!-- Footer for branding --> 1297 <footer class="rally-footer"> 1298 © 2025 Rally LLC. All rights reserved. 1299 <br> 1300 <a href="/terms">Terms of Use</a> | <a href="/privacy">Privacy Policy</a> 1301 </footer>
1302<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4bc70e2c01a94c73b74392e4234840661791215815920" integrity="sha512-L0ha0OXavK/8okipN9F8BtP84dg9DUhPERbBXzwI6dgTA55d2+yweo3pn5CSFYs45/r8md2+xvUPtTdvTNRfjA==" data-cf-beacon='{"version":"2024.11.0","token":"eb101b44180840dd91d7ebf5aec62a82","r":1,"spa":2}' crossorigin="anonymous"></script>
1302 1303</body> 1304</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.