🪻 distributed transcription service thistle.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add subtab query param support

+159 -3
+26 -3
src/components/admin-classes.ts
··· 465 465 466 466 override async connectedCallback() { 467 467 super.connectedCallback(); 468 + 469 + // Check for subtab query parameter 470 + const params = new URLSearchParams(window.location.search); 471 + const subtab = params.get("subtab"); 472 + if (subtab && this.isValidSubtab(subtab)) { 473 + this.activeTab = subtab as "classes" | "waitlist"; 474 + } else { 475 + // Set default subtab in URL if on classes tab 476 + this.setActiveTab(this.activeTab); 477 + } 478 + 468 479 await this.loadData(); 469 480 } 470 481 482 + private isValidSubtab(subtab: string): boolean { 483 + return ["classes", "waitlist"].includes(subtab); 484 + } 485 + 486 + private setActiveTab(tab: "classes" | "waitlist") { 487 + this.activeTab = tab; 488 + // Update URL without reloading page 489 + const url = new URL(window.location.href); 490 + url.searchParams.set("subtab", tab); 491 + window.history.pushState({}, "", url); 492 + } 493 + 471 494 private async loadData() { 472 495 this.isLoading = true; 473 496 this.error = ""; ··· 664 687 <button 665 688 class="tab ${this.activeTab === "classes" ? "active" : ""}" 666 689 @click=${() => { 667 - this.activeTab = "classes"; 690 + this.setActiveTab("classes"); 668 691 }} 669 692 > 670 693 Classes ··· 672 695 <button 673 696 class="tab ${this.activeTab === "waitlist" ? "active" : ""}" 674 697 @click=${() => { 675 - this.activeTab = "waitlist"; 698 + this.setActiveTab("waitlist"); 676 699 }} 677 700 > 678 701 Waitlist ··· 892 915 893 916 await this.loadData(); 894 917 895 - this.activeTab = "classes"; 918 + this.setActiveTab("classes"); 896 919 this.showModal = false; 897 920 this.approvingEntry = null; 898 921 this.meetingTimes = [];
+129
src/components/auth.ts
··· 32 32 @state() passkeySupported = false; 33 33 @state() needsEmailVerification = false; 34 34 @state() verificationCode = ""; 35 + @state() resendCodeTimer = 0; 36 + @state() resendingCode = false; 37 + private resendInterval: number | null = null; 38 + private codeSentAt: number | null = null; // Unix timestamp in seconds when code was sent 35 39 36 40 static override styles = css` 37 41 :host { ··· 283 287 font-family: 'Monaco', 'Courier New', monospace; 284 288 } 285 289 290 + .resend-link { 291 + text-align: center; 292 + margin-top: 1rem; 293 + font-size: 0.875rem; 294 + color: var(--text); 295 + } 296 + 297 + .resend-button { 298 + background: none; 299 + border: none; 300 + color: var(--primary); 301 + cursor: pointer; 302 + text-decoration: underline; 303 + font-size: 0.875rem; 304 + padding: 0; 305 + font-family: inherit; 306 + } 307 + 308 + .resend-button:hover:not(:disabled) { 309 + color: var(--accent); 310 + } 311 + 312 + .resend-button:disabled { 313 + color: var(--secondary); 314 + cursor: not-allowed; 315 + text-decoration: none; 316 + } 317 + 286 318 .btn-secondary { 287 319 background: transparent; 288 320 color: var(--text); ··· 413 445 this.needsEmailVerification = true; 414 446 this.password = ""; 415 447 this.error = ""; 448 + this.startResendTimer(data.verification_code_sent_at); 416 449 return; 417 450 } 418 451 ··· 450 483 this.needsEmailVerification = true; 451 484 this.password = ""; 452 485 this.error = ""; 486 + this.startResendTimer(data.verification_code_sent_at); 453 487 return; 454 488 } 455 489 ··· 571 605 } 572 606 } 573 607 608 + private startResendTimer(sentAtTimestamp: number) { 609 + // Use provided timestamp 610 + this.codeSentAt = sentAtTimestamp; 611 + 612 + // Clear existing interval if any 613 + if (this.resendInterval !== null) { 614 + clearInterval(this.resendInterval); 615 + } 616 + 617 + // Update timer based on elapsed time 618 + const updateTimer = () => { 619 + if (this.codeSentAt === null) return; 620 + 621 + const now = Math.floor(Date.now() / 1000); 622 + const elapsed = now - this.codeSentAt; 623 + const remaining = Math.max(0, (5 * 60) - elapsed); 624 + this.resendCodeTimer = remaining; 625 + 626 + if (remaining <= 0) { 627 + if (this.resendInterval !== null) { 628 + clearInterval(this.resendInterval); 629 + this.resendInterval = null; 630 + } 631 + } 632 + }; 633 + 634 + // Update immediately 635 + updateTimer(); 636 + 637 + // Then update every second 638 + this.resendInterval = window.setInterval(updateTimer, 1000); 639 + } 640 + 641 + private async handleResendCode() { 642 + this.error = ""; 643 + this.resendingCode = true; 644 + 645 + try { 646 + const response = await fetch("/api/auth/resend-verification-code", { 647 + method: "POST", 648 + headers: { 649 + "Content-Type": "application/json", 650 + }, 651 + body: JSON.stringify({ 652 + email: this.email, 653 + }), 654 + }); 655 + 656 + if (!response.ok) { 657 + const data = await response.json(); 658 + this.error = data.error || "Failed to resend code"; 659 + return; 660 + } 661 + 662 + // Start the 5-minute timer 663 + this.startResendTimer(data.verification_code_sent_at); 664 + } catch (error) { 665 + this.error = error instanceof Error ? error.message : "An error occurred"; 666 + } finally { 667 + this.resendingCode = false; 668 + } 669 + } 670 + 671 + private formatTimer(seconds: number): string { 672 + const mins = Math.floor(seconds / 60); 673 + const secs = seconds % 60; 674 + return `${mins}:${secs.toString().padStart(2, '0')}`; 675 + } 676 + 677 + override disconnectedCallback() { 678 + super.disconnectedCallback(); 679 + // Clean up timer when component is removed 680 + if (this.resendInterval !== null) { 681 + clearInterval(this.resendInterval); 682 + this.resendInterval = null; 683 + } 684 + } 685 + 574 686 override render() { 575 687 if (this.loading) { 576 688 return html`<div class="loading">Loading...</div>`; ··· 659 771 ? html`<div class="error-message">${this.error}</div>` 660 772 : "" 661 773 } 774 + 775 + <div class="resend-link"> 776 + ${ 777 + this.resendCodeTimer > 0 778 + ? html`Resend code in ${this.formatTimer(this.resendCodeTimer)}` 779 + : html` 780 + <button 781 + type="button" 782 + class="resend-button" 783 + @click=${this.handleResendCode} 784 + ?disabled=${this.resendingCode} 785 + > 786 + ${this.resendingCode ? "Sending..." : "Resend code"} 787 + </button> 788 + ` 789 + } 790 + </div> 662 791 663 792 <div class="modal-actions"> 664 793 <button
+4
src/pages/admin.html
··· 319 319 // Update URL without reloading 320 320 const url = new URL(window.location.href); 321 321 url.searchParams.set('tab', tabName); 322 + // Remove subtab param when leaving classes tab 323 + if (tabName !== 'classes') { 324 + url.searchParams.delete('subtab'); 325 + } 322 326 window.history.pushState({}, '', url); 323 327 } 324 328 }