180 lines
5.2 KiB
JavaScript
180 lines
5.2 KiB
JavaScript
const createFetchError = async (response) => {
|
|
let payload = {};
|
|
try {
|
|
payload = await response.json();
|
|
} catch (error) {
|
|
payload = {};
|
|
}
|
|
const message = payload.message || payload.error || `Request failed with status ${response.status}`;
|
|
const error = new Error(message);
|
|
error.status = response.status;
|
|
error.code = payload.error;
|
|
error.details = payload.details;
|
|
error.payload = payload;
|
|
return error;
|
|
};
|
|
|
|
let csrfToken = null;
|
|
|
|
const setCsrfToken = (token) => {
|
|
csrfToken = typeof token === 'string' ? token : null;
|
|
};
|
|
|
|
const getCsrfToken = () => csrfToken;
|
|
|
|
const fetchJson = async (url, options = {}) => {
|
|
const method = (options.method || 'GET').toUpperCase();
|
|
const response = await fetch(url, {
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(csrfToken && method !== 'GET' && method !== 'HEAD' ? {'X-CSRF-Token': csrfToken} : {}),
|
|
...(options.headers || {}),
|
|
},
|
|
...options,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw await createFetchError(response);
|
|
}
|
|
|
|
return response.json().catch(() => ({}));
|
|
};
|
|
|
|
const showToast = (message, type = 'success') => {
|
|
const container = document.getElementById('toastContainer');
|
|
if (!container) {
|
|
return;
|
|
}
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast toast--${type}`;
|
|
toast.textContent = message;
|
|
container.appendChild(toast);
|
|
setTimeout(() => {
|
|
toast.classList.add('toast--visible');
|
|
}, 10);
|
|
setTimeout(() => {
|
|
toast.classList.remove('toast--visible');
|
|
setTimeout(() => toast.remove(), 200);
|
|
}, 3200);
|
|
};
|
|
|
|
const setErrorBanner = (bannerEl, message, details) => {
|
|
if (!bannerEl) {
|
|
return;
|
|
}
|
|
const messageEl = bannerEl.querySelector('[data-role="error-message"]');
|
|
const copyButton = bannerEl.querySelector('[data-role="error-copy"]');
|
|
if (messageEl) {
|
|
messageEl.textContent = message;
|
|
}
|
|
if (copyButton) {
|
|
copyButton.onclick = async () => {
|
|
const payload = details || {message};
|
|
try {
|
|
await navigator.clipboard.writeText(JSON.stringify(payload, null, 2));
|
|
showToast('Error details copied.', 'success');
|
|
} catch (error) {
|
|
showToast('Unable to copy details.', 'error');
|
|
}
|
|
};
|
|
}
|
|
bannerEl.hidden = false;
|
|
};
|
|
|
|
const clearErrorBanner = (bannerEl) => {
|
|
if (!bannerEl) {
|
|
return;
|
|
}
|
|
bannerEl.hidden = true;
|
|
};
|
|
|
|
const formatUserDisplay = (user) => {
|
|
if (!user) {
|
|
return 'Not signed in';
|
|
}
|
|
const tag = user.discriminator && user.discriminator !== '0' ? `#${user.discriminator}` : '';
|
|
return `${user.username}${tag}`;
|
|
};
|
|
|
|
const debounce = (callback, wait = 300) => {
|
|
let timeoutId;
|
|
return (...args) => {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
timeoutId = setTimeout(() => callback(...args), wait);
|
|
};
|
|
};
|
|
|
|
const initIndexPage = async () => {
|
|
const sessionStatus = document.getElementById('sessionStatus');
|
|
const loginButton = document.getElementById('loginButton');
|
|
const dashboardButton = document.getElementById('dashboardButton');
|
|
const logoutButton = document.getElementById('logoutButton');
|
|
const banner = document.getElementById('banner');
|
|
|
|
if (!sessionStatus || !loginButton) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const data = await fetchJson('/api/me');
|
|
setCsrfToken(data.csrfToken);
|
|
sessionStatus.textContent = formatUserDisplay(data.user);
|
|
loginButton.hidden = true;
|
|
dashboardButton.hidden = false;
|
|
logoutButton.hidden = false;
|
|
if (banner) {
|
|
banner.textContent = '';
|
|
banner.style.display = 'none';
|
|
}
|
|
} catch (error) {
|
|
sessionStatus.textContent = '';
|
|
loginButton.hidden = false;
|
|
dashboardButton.hidden = true;
|
|
logoutButton.hidden = true;
|
|
if (banner) {
|
|
const sessionExpired = error.status === 401 || error.status === 403;
|
|
banner.textContent = sessionExpired ? '' : 'Unable to check your session. Try again.';
|
|
banner.style.display = sessionExpired ? 'none' : 'block';
|
|
}
|
|
}
|
|
|
|
logoutButton?.addEventListener('click', async () => {
|
|
try {
|
|
await fetchJson('/auth/logout', {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: {
|
|
...(csrfToken ? {'X-CSRF-Token': csrfToken} : {}),
|
|
},
|
|
});
|
|
sessionStatus.textContent = '';
|
|
loginButton.hidden = false;
|
|
dashboardButton.hidden = true;
|
|
logoutButton.hidden = true;
|
|
} catch (error) {
|
|
if (banner) {
|
|
banner.textContent = error.message || 'Failed to log out.';
|
|
banner.style.display = 'block';
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initIndexPage();
|
|
});
|
|
|
|
window.dashboardApi = {
|
|
fetchJson,
|
|
setCsrfToken,
|
|
getCsrfToken,
|
|
showToast,
|
|
setErrorBanner,
|
|
clearErrorBanner,
|
|
formatUserDisplay,
|
|
debounce,
|
|
};
|