fix: implement Blazor-native login form to properly update authentication state
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m26s

Problem: JavaScript login form saved tokens to localStorage but didn't notify
CustomAuthenticationStateProvider, causing [Authorize] pages to remain in
'loading' state indefinitely. The provider only reads tokens when:
1. GetAuthenticationStateAsync() is called (page load)
2. NotifyAuthenticationStateChanged() is triggered (UI updates)

But JavaScript login didn't trigger either, leaving the authentication state
stale.

Solution: Convert AdminLoginForm from HTML+JavaScript to pure Blazor component.
Now the login flow is:
1. User enters credentials in Blazor form
2. HttpClient POST to /api/auth/login
3. Save tokens to localStorage
4. Call CustomAuthenticationStateProvider.LoginAsync() directly
5. Blazor detects auth state change and re-evaluates [Authorize] pages
6. Dashboard [Authorize] page renders successfully

Result: Immediate authentication state update, no loading timeout on protected pages.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 13:03:53 +09:00
parent 041d3cae96
commit 40cffb3beb
326 changed files with 327714 additions and 47 deletions
@@ -0,0 +1,268 @@
/**
* Table Style Generator
* Handles real-time style updates for Bootstrap tables
*/
document.addEventListener('DOMContentLoaded', function() {
// Get elements
const tableStyleForm = document.getElementById('tableStyleForm');
const stylePreviewTable = document.getElementById('stylePreviewTable');
const generatedClasses = document.getElementById('generatedClasses');
const codeClasses = document.getElementById('codeClasses');
const copyStyleBtn = document.getElementById('copyStyleBtn');
const resetStylesBtn = document.getElementById('resetStylesBtn');
const showRowClasses = document.getElementById('showRowClasses');
const collapseColumnsBtn = document.getElementById('collapseColumnsBtn');
const expandColumnsBtn = document.getElementById('expandColumnsBtn');
const tableContainer = document.getElementById('tableContainer');
const headerAccent = document.getElementById('headerAccent');
const tableHeader = stylePreviewTable.querySelector('thead tr');
const styleBordered = document.getElementById('styleBordered');
const styleBorderless = document.getElementById('styleBorderless');
// Handle conflicts between bordered and borderless
styleBordered.addEventListener('change', function() {
if (this.checked && styleBorderless.checked) {
styleBorderless.checked = false;
}
updateTableStyle();
});
styleBorderless.addEventListener('change', function() {
if (this.checked && styleBordered.checked) {
styleBordered.checked = false;
}
updateTableStyle();
});
// Table rows with contextual classes
const tableRows = stylePreviewTable.querySelectorAll('tbody tr');
const contextualClasses = ['table-primary', 'table-secondary', 'table-success', 'table-danger', 'table-warning', 'table-info'];
const rowClasses = Array.from(tableRows).map((row, index) => {
// Assign a specific contextual class based on index
return contextualClasses[index % contextualClasses.length];
});
// Function to update the table style based on form values
function updateTableStyle() {
// Start with base class
const classes = ['table'];
let tableWrapperClass = '';
// Get theme (table-dark, table-light)
const theme = document.querySelector('input[name="tableTheme"]:checked').value;
if (theme) {
classes.push(theme);
}
// Get styles (striped, hover, bordered, borderless)
const styleCheckboxes = document.querySelectorAll('input[name="tableStyle"]:checked');
styleCheckboxes.forEach(checkbox => {
if (checkbox.value === 'table-responsive') {
tableWrapperClass = 'table-responsive';
} else {
classes.push(checkbox.value);
}
});
// Get size (sm, nano)
const size = document.querySelector('input[name="tableSize"]:checked').value;
if (size) {
classes.push(size);
}
// Get accent color for the whole table
const accent = document.getElementById('tableAccent').value;
if (accent) {
classes.push(accent);
}
// Apply header accent using classes only, not inline styles
const headerAccentValue = headerAccent.value;
// First remove any existing background classes
tableHeader.classList.remove(
'bg-primary', 'bg-secondary', 'bg-success',
'bg-danger', 'bg-warning', 'bg-info',
'bg-dark', 'bg-light', 'text-white'
);
// Add the new one if selected
if (headerAccentValue) {
tableHeader.classList.add(headerAccentValue);
// If we're using a dark background, make the text white
if (['bg-primary', 'bg-secondary', 'bg-success', 'bg-danger', 'bg-dark'].includes(headerAccentValue)) {
tableHeader.classList.add('text-white');
}
}
// Get caption position (now using radio buttons)
const captionPosition = document.querySelector('input[name="captionPosition"]:checked').value;
if (captionPosition) {
stylePreviewTable.classList.remove('caption-top', 'caption-bottom');
stylePreviewTable.classList.add(captionPosition);
} else {
stylePreviewTable.classList.remove('caption-top', 'caption-bottom');
}
// Check if we should show row contextual classes
if (showRowClasses.checked) {
tableRows.forEach((row, index) => {
// First remove any existing contextual classes
row.classList.remove('table-primary', 'table-secondary', 'table-success',
'table-danger', 'table-warning', 'table-info');
// Add the contextual class based on index
if (index < rowClasses.length) {
row.classList.add(rowClasses[index]);
}
});
} else {
// Remove all contextual classes from rows
tableRows.forEach(row => {
row.classList.remove('table-primary', 'table-secondary', 'table-success',
'table-danger', 'table-warning', 'table-info');
});
}
// Update the table classes
stylePreviewTable.className = classes.join(' ');
// Update wrapper for responsiveness
if (tableWrapperClass && !tableContainer.classList.contains(tableWrapperClass)) {
tableContainer.className = tableWrapperClass;
} else if (!tableWrapperClass) {
tableContainer.className = '';
}
// Update the generated classes display and note about additional header styling if applied
const displayClasses = classes.join(' ');
let displayText = displayClasses;
// Add note about header styling if applied
if (headerAccentValue) {
displayText += `\n\n<!-- Additional header styling -->\n<thead>\n <tr class="${headerAccentValue}${['bg-primary', 'bg-secondary', 'bg-success', 'bg-danger', 'bg-dark'].includes(headerAccentValue) ? ' text-white' : ''}">\n <!-- your header cells here -->\n </tr>\n</thead>`;
}
// Add note about contextual row classes if enabled
if (showRowClasses.checked) {
displayText += `\n\n<!-- Row contextual classes -->\n<tbody>\n <tr class="table-primary">...</tr>\n <tr class="table-secondary">...</tr>\n <tr class="table-success">...</tr>\n <tr class="table-danger">...</tr>\n <tr class="table-warning">...</tr>\n <tr class="table-info">...</tr>\n</tbody>`;
}
generatedClasses.textContent = displayText;
codeClasses.textContent = displayClasses;
}
// Add event listeners to all form controls
tableStyleForm.addEventListener('change', updateTableStyle);
// Copy button functionality
copyStyleBtn.addEventListener('click', function() {
// Create a textarea element to copy from
const textarea = document.createElement('textarea');
textarea.value = generatedClasses.textContent;
document.body.appendChild(textarea);
textarea.select();
try {
// Execute copy command
document.execCommand('copy');
// Show success feedback
const originalText = copyStyleBtn.innerHTML;
copyStyleBtn.innerHTML = '<svg class="sa-icon me-1"><use href="icons/sprite.svg#check"/></svg> Copied!';
// Reset button text after delay
setTimeout(() => {
copyStyleBtn.innerHTML = originalText;
}, 2000);
} catch (err) {
console.error('Could not copy text: ', err);
}
// Remove the temporary textarea
document.body.removeChild(textarea);
});
// Reset button functionality
resetStylesBtn.addEventListener('click', function() {
// Reset form controls
document.getElementById('themeDefault').checked = true;
document.getElementById('sizeDefault').checked = true;
document.getElementById('tableAccent').value = '';
document.getElementById('headerAccent').value = '';
document.getElementById('captionDefault').checked = true;
document.getElementById('showRowClasses').checked = false;
document.getElementById('expandColumnsBtn').checked = true;
// Reset checkboxes
const checkboxes = document.querySelectorAll('input[name="tableStyle"]');
checkboxes.forEach(checkbox => {
if (checkbox.id === 'styleResponsive') {
checkbox.checked = true;
} else {
checkbox.checked = false;
}
});
// Restore columns to default view
expandColumns();
// Update table style
updateTableStyle();
});
// Function to hide columns
function collapseColumns() {
// Hide columns with d-md-table-cell and d-lg-table-cell classes
const mdCells = document.querySelectorAll('.d-md-table-cell');
mdCells.forEach(cell => {
cell.classList.add('d-none');
cell.classList.remove('d-md-table-cell');
});
const lgCells = document.querySelectorAll('.d-lg-table-cell');
lgCells.forEach(cell => {
cell.classList.add('d-none');
cell.classList.remove('d-lg-table-cell');
});
}
// Function to show all columns
function expandColumns() {
// Restore hidden columns
const hiddenCells = document.querySelectorAll('td.d-none, th.d-none');
hiddenCells.forEach(cell => {
if (cell.textContent.includes('Address') || cell.textContent.includes('City') ||
cell.parentElement.cells[4] === cell || cell.parentElement.cells[5] === cell) {
cell.classList.remove('d-none');
cell.classList.add('d-md-table-cell');
} else if (cell.textContent.includes('State') || cell.textContent.includes('Country') ||
cell.parentElement.cells[6] === cell || cell.parentElement.cells[7] === cell) {
cell.classList.remove('d-none');
cell.classList.add('d-lg-table-cell');
}
});
}
// Toggle columns for responsive preview - updated for radio buttons
collapseColumnsBtn.addEventListener('change', function() {
if (this.checked) {
collapseColumns();
}
});
expandColumnsBtn.addEventListener('change', function() {
if (this.checked) {
expandColumns();
}
});
// Hide row contextual classes by default
if (!showRowClasses.checked) {
tableRows.forEach(row => {
row.classList.remove('table-primary', 'table-secondary', 'table-success',
'table-danger', 'table-warning', 'table-info');
});
}
// Initialize the table with default styles
updateTableStyle();
});