-
Notifications
You must be signed in to change notification settings - Fork 0
Add auth-aware toolbar and landing CTAs #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add auth-aware toolbar and landing CTAs #36
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds authentication-aware UI components and synchronizes cloud features across the application. The changes integrate user authentication state into the toolbar and landing page, enabling conditional rendering of cloud-related actions based on sign-in status.
Key Changes:
- Toolbar now directly manages user state with new
setUser()andclearUser()APIs that render user info and control cloud action visibility - Landing page "Save to Cloud" button dynamically updates based on authentication state, routing to dashboard when signed in
- Authentication flow enhanced with post-auth action queuing to handle pending dashboard requests after sign-in
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| src/ui/components/Toolbar.ts | Added setUser/clearUser methods to manage authentication UI state, cloud action button state management, and replaced UserMenu dependency with inline rendering |
| src/ui/components/LandingPageEnhanced.ts | Added setAuthState method and "Save to Cloud" button that syncs with authentication state, dynamically updating label and styling |
| src/ui/App.ts | Removed lazy-loading imports, integrated cloud sync initialization with toolbar/landing page state updates, added post-auth action handling for deferred dashboard navigation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!this.cloudSync) { | ||
| this.initializeCloudSync(); | ||
| } | ||
| this.handleShowDashboard(); |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Missing error handling - If this.cloudSync is null when calling handleShowDashboard() (line 1559), it will show an error toast but the user remains on the landing page without clear indication that they need to re-authenticate. Consider redirecting to the authentication modal or showing a more actionable error message that prompts re-authentication.
|
|
||
| this.cloudActionButtons.forEach((btn) => { | ||
| btn.disabled = !authenticated; | ||
| btn.setAttribute('aria-disabled', (!authenticated).toString()); |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessibility: Inconsistent disabled state implementation - The cloud action buttons use aria-disabled and visual styling (opacity, cursor) to indicate disabled state, but don't use the native disabled attribute. This creates an inconsistent experience where assistive technology users hear the button as disabled, but it can still receive focus and keyboard activation. Either use the native disabled attribute or handle keyboard events to prevent activation when disabled.
| btn.setAttribute('aria-disabled', (!authenticated).toString()); |
| container.innerHTML = ` | ||
| <div style="padding: 12px; border: 1px solid var(--border-color); border-radius: 10px; background: var(--bg-secondary); display: flex; flex-direction: column; gap: 10px;"> | ||
| <div style="display: flex; align-items: center; gap: 10px;"> | ||
| <div style="width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, var(--primary-color), #667eea); color: white; display: flex; align-items: center; justify-content: center; font-weight: 700;">${initials}</div> | ||
| <div style="display: flex; flex-direction: column;"> | ||
| <span style="font-weight: 600; color: var(--text-primary);">${user.email}</span> | ||
| <span style="font-size: 12px; color: var(--text-secondary);">Signed in · Cloud ready</span> | ||
| </div> | ||
| </div> | ||
| <div style="display: flex; gap: 8px;"> | ||
| <button class="btn btn-secondary cloud-action" id="toolbar-dashboard" aria-label="Open cloud dashboard"> | ||
| <span>Open Dashboard</span> | ||
| </button> | ||
| <button class="btn btn-secondary" id="toolbar-logout" aria-label="Sign out" style="background: #111827; color: white;"> | ||
| <span>Logout</span> | ||
| </button> | ||
| </div> | ||
| </div> | ||
| `; | ||
|
|
||
| const dashboardBtn = container.querySelector('#toolbar-dashboard') as HTMLButtonElement; | ||
| const logoutBtn = container.querySelector('#toolbar-logout') as HTMLButtonElement; | ||
|
|
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security: Potential XSS vulnerability - The user email is inserted directly into the HTML without sanitization using template literals. If the email contains HTML/JavaScript, it could be executed. Use textContent or escape the email value before inserting it into the DOM.
Example fix:
const emailSpan = document.createElement('span');
emailSpan.style.fontWeight = '600';
emailSpan.style.color = 'var(--text-primary)';
emailSpan.textContent = user.email;| container.innerHTML = ` | |
| <div style="padding: 12px; border: 1px solid var(--border-color); border-radius: 10px; background: var(--bg-secondary); display: flex; flex-direction: column; gap: 10px;"> | |
| <div style="display: flex; align-items: center; gap: 10px;"> | |
| <div style="width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, var(--primary-color), #667eea); color: white; display: flex; align-items: center; justify-content: center; font-weight: 700;">${initials}</div> | |
| <div style="display: flex; flex-direction: column;"> | |
| <span style="font-weight: 600; color: var(--text-primary);">${user.email}</span> | |
| <span style="font-size: 12px; color: var(--text-secondary);">Signed in · Cloud ready</span> | |
| </div> | |
| </div> | |
| <div style="display: flex; gap: 8px;"> | |
| <button class="btn btn-secondary cloud-action" id="toolbar-dashboard" aria-label="Open cloud dashboard"> | |
| <span>Open Dashboard</span> | |
| </button> | |
| <button class="btn btn-secondary" id="toolbar-logout" aria-label="Sign out" style="background: #111827; color: white;"> | |
| <span>Logout</span> | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| const dashboardBtn = container.querySelector('#toolbar-dashboard') as HTMLButtonElement; | |
| const logoutBtn = container.querySelector('#toolbar-logout') as HTMLButtonElement; | |
| // Build user menu DOM elements programmatically to avoid XSS | |
| container.innerHTML = ''; | |
| const wrapper = document.createElement('div'); | |
| wrapper.style.padding = '12px'; | |
| wrapper.style.border = '1px solid var(--border-color)'; | |
| wrapper.style.borderRadius = '10px'; | |
| wrapper.style.background = 'var(--bg-secondary)'; | |
| wrapper.style.display = 'flex'; | |
| wrapper.style.flexDirection = 'column'; | |
| wrapper.style.gap = '10px'; | |
| const topRow = document.createElement('div'); | |
| topRow.style.display = 'flex'; | |
| topRow.style.alignItems = 'center'; | |
| topRow.style.gap = '10px'; | |
| const avatar = document.createElement('div'); | |
| avatar.style.width = '36px'; | |
| avatar.style.height = '36px'; | |
| avatar.style.borderRadius = '50%'; | |
| avatar.style.background = 'linear-gradient(135deg, var(--primary-color), #667eea)'; | |
| avatar.style.color = 'white'; | |
| avatar.style.display = 'flex'; | |
| avatar.style.alignItems = 'center'; | |
| avatar.style.justifyContent = 'center'; | |
| avatar.style.fontWeight = '700'; | |
| avatar.textContent = initials; | |
| const infoCol = document.createElement('div'); | |
| infoCol.style.display = 'flex'; | |
| infoCol.style.flexDirection = 'column'; | |
| const emailSpan = document.createElement('span'); | |
| emailSpan.style.fontWeight = '600'; | |
| emailSpan.style.color = 'var(--text-primary)'; | |
| emailSpan.textContent = user.email; | |
| const signedInSpan = document.createElement('span'); | |
| signedInSpan.style.fontSize = '12px'; | |
| signedInSpan.style.color = 'var(--text-secondary)'; | |
| signedInSpan.textContent = 'Signed in · Cloud ready'; | |
| infoCol.appendChild(emailSpan); | |
| infoCol.appendChild(signedInSpan); | |
| topRow.appendChild(avatar); | |
| topRow.appendChild(infoCol); | |
| const actionsRow = document.createElement('div'); | |
| actionsRow.style.display = 'flex'; | |
| actionsRow.style.gap = '8px'; | |
| const dashboardBtn = document.createElement('button'); | |
| dashboardBtn.className = 'btn btn-secondary cloud-action'; | |
| dashboardBtn.id = 'toolbar-dashboard'; | |
| dashboardBtn.setAttribute('aria-label', 'Open cloud dashboard'); | |
| const dashboardSpan = document.createElement('span'); | |
| dashboardSpan.textContent = 'Open Dashboard'; | |
| dashboardBtn.appendChild(dashboardSpan); | |
| const logoutBtn = document.createElement('button'); | |
| logoutBtn.className = 'btn btn-secondary'; | |
| logoutBtn.id = 'toolbar-logout'; | |
| logoutBtn.setAttribute('aria-label', 'Sign out'); | |
| logoutBtn.style.background = '#111827'; | |
| logoutBtn.style.color = 'white'; | |
| const logoutSpan = document.createElement('span'); | |
| logoutSpan.textContent = 'Logout'; | |
| logoutBtn.appendChild(logoutSpan); | |
| actionsRow.appendChild(dashboardBtn); | |
| actionsRow.appendChild(logoutBtn); | |
| wrapper.appendChild(topRow); | |
| wrapper.appendChild(actionsRow); | |
| container.appendChild(wrapper); | |
| // Query buttons for event listeners | |
| // (already have references: dashboardBtn, logoutBtn) |
| container.innerHTML = ` | ||
| <button class="login-button" style="width: 100%; padding: 0.75rem; background: linear-gradient(135deg, var(--primary-color), #667eea); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: transform 0.2s, box-shadow 0.2s;" aria-label="Sign in to cloud storage"> | ||
| <svg style="width: 18px; height: 18px; vertical-align: middle; margin-right: 8px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/> | ||
| <circle cx="12" cy="7" r="4"/> | ||
| </svg> | ||
| <span style="vertical-align: middle;">Sign In</span> | ||
| </button> | ||
| `; | ||
|
|
||
| const button = container.querySelector('.login-button') as HTMLButtonElement; | ||
| button?.addEventListener('click', () => this.onShowAuth?.()); | ||
| button?.addEventListener('mouseenter', () => { | ||
| button.style.transform = 'translateY(-2px)'; | ||
| button.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; | ||
| }); | ||
| button?.addEventListener('mouseleave', () => { | ||
| button.style.transform = 'translateY(0)'; | ||
| button.style.boxShadow = 'none'; | ||
| }); |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maintainability: Duplicated login button HTML - The entire login button HTML structure and event listeners are duplicated from the removed showLoginButton method. This creates maintenance burden where changes need to be made in multiple places. Since showLoginButton now just calls clearUser(), consider consolidating all login button logic into clearUser().
| if (!this.cloudSync) { | ||
| this.initializeCloudSync(); | ||
| } | ||
| this.handleShowDashboard(); |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Race condition in cloud sync initialization - If handleLandingSaveToCloud() is called when authenticated but cloudSync is null, it calls initializeCloudSync() and immediately calls handleShowDashboard() on line 1559. However, initializeCloudSync() is synchronous and doesn't wait for any async initialization. Then handleShowDashboard() checks if cloudSync exists but it was just set on line 1450, so this might work - but the pattern is fragile. If initializeCloudSync() had any async operations, this would fail. Consider returning early and showing a loading state, or ensure cloud sync is fully initialized before proceeding to the dashboard.
| this.cloudActionButtons = Array.from( | ||
| this.element.querySelectorAll('.cloud-action') | ||
| ) as HTMLButtonElement[]; |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maintainability: Code duplication - The cloud action buttons query is duplicated in three places (constructor line 60, setUser line 300, clearUser line 334). This violates DRY principle and makes maintenance harder. Consider extracting this into a private method like refreshCloudActionButtons() that both setUser and clearUser can call.
| : 'Sign in to start saving redactions to the cloud'; | ||
| this.saveToCloudButton.style.opacity = this.isAuthenticated ? '1' : '0.75'; | ||
| this.saveToCloudButton.style.filter = this.isAuthenticated ? 'none' : 'grayscale(0.2)'; | ||
| this.saveToCloudButton.setAttribute('aria-disabled', (!this.isAuthenticated).toString()); |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessibility: Button not truly disabled - The button uses aria-disabled attribute but is not actually disabled with the disabled property. This means keyboard users and screen reader users will perceive it as disabled, but mouse users can still click it. For consistency, either fully disable the button with btn.disabled = true and remove the click handler, or allow clicking but show an appropriate message prompting the user to sign in.
| const label = this.saveToCloudButton.querySelector('.save-cloud-label'); | ||
| if (label) { | ||
| label.textContent = this.isAuthenticated ? 'Open Cloud Dashboard' : 'Save to Cloud'; | ||
| } |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Performance: Redundant DOM queries - syncSaveToCloudButton() queries the DOM for .save-cloud-label on every call. Since the button structure is static after creation, consider caching the label element as a class property (e.g., private saveToCloudLabel: HTMLElement | null) when creating the button, to avoid repeated DOM queries.
Summary
Testing
Codex Task