Migrate frontend from monolithic app.js to vanilla ES modules
- Delete React files (App.jsx, main.jsx, App.css) - Create modular vanilla JS structure in client/src/: - state.js (global state object) - api.js (all fetch calls) - sse.js (SSE connection management) - ui/auth.js (authentication UI) - ui/downloads.js (downloads rendering) - ui/history.js (history section) - ui/statusPanel.js (status panel) - ui/webhooks.js (webhook management) - ui/filters.js (download client filter) - ui/theme.js (theme switching) - ui/tabs.js (tab navigation) - utils/format.js (formatting utilities) - utils/storage.js (localStorage helpers) - main.js (DOMContentLoaded bootstrap) - Update vite.config.js for vanilla build outputting to ../public/app.js - Build succeeds: 14 modules, 43.88 kB output
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||
|
||||
import { state, SPLASH_MIN_MS } from '../state.js';
|
||||
import { checkAuthentication, handleLogin as apiHandleLogin, handleLogout as apiHandleLogout } from '../api.js';
|
||||
import { startSSE, stopSSE } from '../sse.js';
|
||||
import { stopHistoryRefresh, clearHistory, startHistoryRefresh } from './history.js';
|
||||
import { closeStatusPanel } from './statusPanel.js';
|
||||
|
||||
export function fadeOutLogin() {
|
||||
return new Promise(resolve => {
|
||||
const login = document.getElementById('login-container');
|
||||
login.classList.add('fade-out');
|
||||
login.addEventListener('transitionend', () => {
|
||||
login.style.display = 'none';
|
||||
login.classList.remove('fade-out');
|
||||
resolve();
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
export function showSplash() {
|
||||
const splash = document.getElementById('splash-screen');
|
||||
splash.style.display = 'flex';
|
||||
splash.style.opacity = '1';
|
||||
splash.classList.remove('fade-out');
|
||||
}
|
||||
|
||||
export function dismissSplash(startTime) {
|
||||
return new Promise(resolve => {
|
||||
const elapsed = Date.now() - (startTime || 0);
|
||||
const remaining = Math.max(0, SPLASH_MIN_MS - elapsed);
|
||||
setTimeout(() => {
|
||||
const splash = document.getElementById('splash-screen');
|
||||
splash.classList.add('fade-out');
|
||||
// Fallback: resolve after transition duration + buffer in case
|
||||
// transitionend never fires (e.g. display was toggled in same frame)
|
||||
const TRANSITION_MS = 400;
|
||||
const fallback = setTimeout(() => {
|
||||
splash.style.display = 'none';
|
||||
resolve();
|
||||
}, TRANSITION_MS + 100);
|
||||
splash.addEventListener('transitionend', () => {
|
||||
clearTimeout(fallback);
|
||||
splash.style.display = 'none';
|
||||
resolve();
|
||||
}, { once: true });
|
||||
}, remaining);
|
||||
});
|
||||
}
|
||||
|
||||
export async function checkAuthenticationAndInit() {
|
||||
const splashStart = Date.now();
|
||||
try {
|
||||
const result = await checkAuthentication();
|
||||
if (result.authenticated) {
|
||||
showDashboard();
|
||||
showLoading();
|
||||
startSSE();
|
||||
await dismissSplash(splashStart);
|
||||
} else {
|
||||
await dismissSplash(splashStart);
|
||||
showLogin();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Authentication check failed:', err);
|
||||
await dismissSplash(splashStart);
|
||||
showLogin();
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const rememberMe = document.getElementById('remember-me').checked;
|
||||
|
||||
try {
|
||||
const result = await apiHandleLogin(username, password, rememberMe);
|
||||
|
||||
if (result.success) {
|
||||
// Fade out login, then show splash while opening SSE stream.
|
||||
// requestAnimationFrame ensures the browser paints the splash at
|
||||
// opacity:1 before dismissSplash adds fade-out, so the CSS
|
||||
// transition fires and transitionend is guaranteed.
|
||||
await fadeOutLogin();
|
||||
showSplash();
|
||||
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||
showDashboard();
|
||||
showLoading();
|
||||
const splashStart = Date.now();
|
||||
startSSE();
|
||||
await dismissSplash(splashStart);
|
||||
} else {
|
||||
showLoginError(result.error || 'Login failed');
|
||||
}
|
||||
} catch (err) {
|
||||
showLoginError('Login failed. Please try again.');
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleLogoutClick() {
|
||||
try {
|
||||
stopSSE();
|
||||
stopHistoryRefresh();
|
||||
if (statusRefreshHandle) { clearInterval(statusRefreshHandle); statusRefreshHandle.value = null; }
|
||||
await apiHandleLogout();
|
||||
currentUser = null;
|
||||
clearHistory();
|
||||
showLogin();
|
||||
} catch (err) {
|
||||
console.error('Logout failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
export function showLogin() {
|
||||
document.getElementById('login-container').style.display = 'flex';
|
||||
document.getElementById('dashboard-container').style.display = 'none';
|
||||
hideLoginError();
|
||||
}
|
||||
|
||||
export function showDashboard() {
|
||||
document.getElementById('login-container').style.display = 'none';
|
||||
document.getElementById('dashboard-container').style.display = 'block';
|
||||
document.getElementById('currentUser').textContent = state.currentUser.name || '-';
|
||||
// Always start with status panel hidden (guards against stale display value on re-login)
|
||||
const sp = document.getElementById('status-panel');
|
||||
sp.style.display = 'none';
|
||||
// Also hide webhooks-section to keep them in sync (both show/hide together)
|
||||
const webhooksSection = document.getElementById('webhooks-section');
|
||||
if (webhooksSection) webhooksSection.style.display = 'none';
|
||||
document.getElementById('admin-controls').style.display = state.isAdmin ? 'flex' : 'none';
|
||||
// Note: webhooks-section visibility is controlled by toggleStatusPanel()
|
||||
// Initialise days input from saved value
|
||||
const daysInput = document.getElementById('history-days');
|
||||
if (daysInput) daysInput.value = state.historyDays;
|
||||
startHistoryRefresh();
|
||||
}
|
||||
|
||||
export function showLoginError(message) {
|
||||
const errorDiv = document.getElementById('login-error');
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
export function hideLoginError() {
|
||||
const errorDiv = document.getElementById('login-error');
|
||||
errorDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
export function showError(message) {
|
||||
const errorDiv = document.getElementById('error-message');
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
export function hideError() {
|
||||
const errorDiv = document.getElementById('error-message');
|
||||
errorDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
export function showLoading() {
|
||||
const loading = document.getElementById('loading');
|
||||
loading.style.display = 'block';
|
||||
}
|
||||
|
||||
export function hideLoading() {
|
||||
const loading = document.getElementById('loading');
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
Reference in New Issue
Block a user