507 lines
13 KiB
JavaScript
507 lines
13 KiB
JavaScript
const DEBUG_ON = true;
|
|
|
|
// served from same path as TWTXT file
|
|
const TWTXT_FILE_URL = window.location.pathname;
|
|
const REMEMBER_LOGIN_STORAGE_KEY = 'rememberLogin';
|
|
const ACCESS_TOKEN_COOKIE_KEY = 'accessToken';
|
|
|
|
const debug = (...vals) => {
|
|
if (DEBUG_ON) console.log(...vals);
|
|
};
|
|
|
|
export default (async () => {
|
|
/* DOM Elements */
|
|
const twtForm = document.getElementById('twtForm'),
|
|
loginForm = document.getElementById('loginControls-form'),
|
|
fileBox = document.getElementById('fileBox'),
|
|
fileContentsSection = document.getElementById('fileContentsSection'),
|
|
toastContainer = document.getElementById('toast-container'),
|
|
twtControlsContentInput = document.getElementById(
|
|
'twtControlsContentInput'
|
|
),
|
|
twtSubmitButton = document.querySelector('.twtControls-submitButton'),
|
|
twtLogoutButton = document.getElementById('twtControlsLogoutButton'),
|
|
twtFileEditButton = document.getElementById('twtControlsEditButton'),
|
|
menuCheckbox = document.getElementById('hamburgerToggleCheckbox'),
|
|
twtxtEditFormText = document.getElementById('twtxtEditFormText'),
|
|
uploadInputs = document.querySelectorAll('.twtControls-uploadInput'),
|
|
rememberToggle = document.getElementById('loginControls-rememberToggle');
|
|
|
|
const lastModifiedDates = {};
|
|
let isEditing = false,
|
|
cookie,
|
|
fileText,
|
|
token;
|
|
|
|
const showToast = (message, type = 'success') => {
|
|
const toast = document.createElement('div');
|
|
toast.classList.add('toast');
|
|
if (type === 'error') toast.classList.add('error');
|
|
toast.textContent = message;
|
|
|
|
toastContainer.appendChild(toast);
|
|
setTimeout(() => {
|
|
toast.style.animation = 'fadeOut 0.5s forwards';
|
|
setTimeout(() => toast.remove(), 500);
|
|
}, 3000);
|
|
};
|
|
|
|
const beginEditMode = () => {
|
|
isEditing = true;
|
|
|
|
if (menuCheckbox) menuCheckbox.checked = false;
|
|
if (twtSubmitButton) twtSubmitButton.setAttribute('disabled', 'disabled');
|
|
document.body.classList.add('js-editMode');
|
|
};
|
|
|
|
const endEditMode = () => {
|
|
isEditing = false;
|
|
|
|
if (twtSubmitButton) twtSubmitButton.removeAttribute('disabled');
|
|
document.body.classList.remove('js-editMode');
|
|
};
|
|
|
|
const getCookie = (name) => {
|
|
const cookies = document.cookie.split('; ');
|
|
for (const cookie of cookies) {
|
|
const [key, value] = cookie.split('=');
|
|
if (key === name) return decodeURIComponent(value);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const setCookie = (name, value, expireDays) => {
|
|
const isSecure = window.location.protocol === 'https';
|
|
const expireDate = new Date(); // current date
|
|
expireDate.setTime(
|
|
expireDate.getTime() + (expireDays ?? 0) * 24 * 60 * 60 * 1000
|
|
);
|
|
let expires =
|
|
expireDays !== undefined ? `expires=${expireDate.toUTCString()}; ` : '';
|
|
document.cookie = `${name}=${encodeURIComponent(value)}; ${expires}${isSecure ? 'Secure; ' : ''}SameSite=Strict; Path=/`;
|
|
};
|
|
|
|
const loadTwtxtFile = async (filePath = TWTXT_FILE_URL) => {
|
|
debug('loadTwtxtFile start');
|
|
let response;
|
|
const fileURL = `${filePath.charAt(0) !== '/' ? '/' : ''}${filePath}`;
|
|
|
|
const headers = new Headers();
|
|
headers.set(
|
|
'Accept',
|
|
'application/twtxt+txt,text/twtxt,text/plain;q=0.9,*/*;q=0.8'
|
|
);
|
|
|
|
if (lastModifiedDates[fileURL]) {
|
|
headers['If-Modified-Since'] = lastModifiedDates[fileURL];
|
|
}
|
|
|
|
try {
|
|
response = await fetch(fileURL, {
|
|
method: 'GET',
|
|
headers,
|
|
mode: 'same-origin',
|
|
});
|
|
|
|
if (response?.body) fileText = await new Response(response.body).text();
|
|
|
|
if (response?.headers.get('Last-Modified')) {
|
|
lastModifiedDates[fileURL] = response?.headers.get('Last-Modified');
|
|
}
|
|
|
|
if (fileText && fileBox) {
|
|
fileBox.textContent = fileText;
|
|
}
|
|
} catch (err) {
|
|
showToast('Unable to load file, please try again later.', 'error');
|
|
console.error('Error loading file', err);
|
|
}
|
|
debug('loadTwtxtFile end');
|
|
};
|
|
|
|
const refreshToken = async (hideToast = false) => {
|
|
debug('refreshToken start', hideToast);
|
|
const rememberToggleVal =
|
|
localStorage.getItem(REMEMBER_LOGIN_STORAGE_KEY) === 'true';
|
|
|
|
const res = await fetch(`${TWTXT_FILE_URL}`, {
|
|
method: 'POST',
|
|
body: new URLSearchParams({
|
|
rememberToggle: rememberToggleVal,
|
|
type: 'refresh',
|
|
}),
|
|
credentials: 'include', // Include cookies
|
|
});
|
|
|
|
if (res.ok && res?.body) {
|
|
token = await new Response(res.body).text();
|
|
if (rememberToggleVal) {
|
|
debug('refreshToken set new accessToken cookie');
|
|
setCookie(ACCESS_TOKEN_COOKIE_KEY, token);
|
|
}
|
|
debug('refreshToken end OK');
|
|
return;
|
|
}
|
|
|
|
// Handle refresh failure
|
|
if (!hideToast)
|
|
showToast('Unable to refresh token, please try again later.', 'error');
|
|
token = undefined;
|
|
document.body.classList.remove('js-authorized');
|
|
debug('refreshToken end error');
|
|
throw new Error('Failed to refresh token');
|
|
};
|
|
|
|
const uploadFiles = async (files, uploadRoute, secondAttempt = false) => {
|
|
if (!uploadRoute) return;
|
|
|
|
debug('uploadFiles', token, files, uploadRoute, secondAttempt);
|
|
|
|
const formData = new FormData();
|
|
for (let i = 0; i < files.length; i++) {
|
|
formData.append('files', files[i]);
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(uploadRoute, {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (res.ok) {
|
|
showToast(`File${files.length !== 1 ? 's' : ''} uploaded`);
|
|
|
|
const filePath = await res.text();
|
|
twtControlsContentInput.value += filePath
|
|
.split('\n')
|
|
.map((currFilePath) =>
|
|
[
|
|
' ',
|
|
location.protocol,
|
|
'//',
|
|
location.hostname,
|
|
location.protocol !== 'https' && location.port !== 80
|
|
? ':' + location.port
|
|
: '',
|
|
currFilePath,
|
|
].join('')
|
|
)
|
|
.join('');
|
|
|
|
return;
|
|
}
|
|
|
|
if (!secondAttempt) {
|
|
await refreshToken();
|
|
return uploadFiles(files, uploadRoute, true);
|
|
}
|
|
|
|
showToast(
|
|
`Unable to upload image${files.length !== 1 ? 's' : ''} refresh token, please try again later.`,
|
|
'error'
|
|
);
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
};
|
|
|
|
/* Handlers */
|
|
|
|
const dragOverHandler = (ev) => {
|
|
const files = [...ev.dataTransfer.items].filter(
|
|
(item) => item.kind === 'file'
|
|
);
|
|
|
|
if (files.length > 0) {
|
|
ev.preventDefault();
|
|
ev.dataTransfer.dropEffect = 'copy';
|
|
}
|
|
};
|
|
|
|
const dragOverWindowHandler = (ev) => {
|
|
const files = [...ev.dataTransfer.items].filter(
|
|
(item) => item.kind === 'file'
|
|
);
|
|
|
|
if (files.length > 0) {
|
|
ev.preventDefault();
|
|
|
|
if (!twtControlsContentInput.contains(ev.target)) {
|
|
ev.dataTransfer.dropEffect = 'none';
|
|
}
|
|
}
|
|
};
|
|
|
|
const dropHandler = (ev) => {
|
|
ev.preventDefault();
|
|
|
|
if (!uploadInputs.length) return;
|
|
|
|
const files = [...ev.dataTransfer.items]
|
|
.map((item) => item.getAsFile())
|
|
.filter((file) => file);
|
|
|
|
debug('dropHandler', files);
|
|
uploadFiles(files, uploadInputs[0].getAttribute('data-route'));
|
|
};
|
|
|
|
const dropWindowHandler = (ev) => {
|
|
if ([...ev.dataTransfer.items].some((item) => item.kind === 'file')) {
|
|
ev.preventDefault();
|
|
}
|
|
};
|
|
|
|
const editClickHandler = () => {
|
|
if (isEditing) return;
|
|
|
|
if (twtxtEditFormText) twtxtEditFormText.value = fileText;
|
|
|
|
beginEditMode();
|
|
};
|
|
|
|
const editResetHandler = (ev) => {
|
|
ev.preventDefault();
|
|
|
|
if (!isEditing && !confirm('Do you want to quit editing?')) return;
|
|
|
|
if (fileText && fileBox) {
|
|
fileBox.textContent = fileText;
|
|
}
|
|
|
|
endEditMode();
|
|
};
|
|
|
|
const editSubmitHandler = async (ev, secondAttempt = false) => {
|
|
ev?.preventDefault();
|
|
|
|
try {
|
|
const newFileText = twtxtEditFormText.value;
|
|
|
|
debug('edit file submit', { newFileText });
|
|
if (!newFileText) return;
|
|
|
|
const res = await fetch(`${TWTXT_FILE_URL}`, {
|
|
method: 'PUT',
|
|
body: new URLSearchParams({
|
|
fileContents: newFileText,
|
|
}),
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!res.ok && !secondAttempt) {
|
|
await refreshToken();
|
|
return editSubmitHandler(ev, true);
|
|
}
|
|
|
|
showToast('File updated');
|
|
|
|
if (newFileText && fileBox) {
|
|
fileText = newFileText;
|
|
fileBox.textContent = newFileText;
|
|
}
|
|
|
|
endEditMode();
|
|
|
|
await loadTwtxtFile();
|
|
} catch (err) {
|
|
if (!secondAttempt) {
|
|
await refreshToken();
|
|
return twtFormSubmitHandler(ev, true);
|
|
}
|
|
|
|
showToast('Unable to update file, please try again later.', 'error');
|
|
console.error('Error updating file', err);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
const loginFormSubmitHandler = async (ev) => {
|
|
ev?.preventDefault();
|
|
let response;
|
|
|
|
try {
|
|
const loginData = new URLSearchParams(new FormData(loginForm));
|
|
debug('loginForm submit', { loginData });
|
|
|
|
response = await fetch(TWTXT_FILE_URL, {
|
|
method: 'POST',
|
|
body: loginData,
|
|
mode: 'same-origin',
|
|
credentials: 'include',
|
|
});
|
|
|
|
debug('loginForm submit', { response });
|
|
|
|
if (!response.ok) throw new Error(response.statusText);
|
|
showToast('Login complete');
|
|
} catch (err) {
|
|
showToast('Unable to login, please try again later.', 'error');
|
|
console.error('Error logging in', err);
|
|
|
|
return;
|
|
}
|
|
|
|
if (response?.body) token = await new Response(response.body).text();
|
|
|
|
if (token) document.body.classList.add('js-authorized');
|
|
|
|
if (token && rememberToggle.checked)
|
|
setCookie(ACCESS_TOKEN_COOKIE_KEY, token, 7);
|
|
|
|
debug('loginForm submit', { cookie, token, response });
|
|
|
|
return false;
|
|
};
|
|
|
|
const logoutHandler = async () => {
|
|
try {
|
|
const res = await fetch(`${TWTXT_FILE_URL}`, {
|
|
method: 'POST',
|
|
body: new URLSearchParams({
|
|
type: 'logout',
|
|
}),
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!res.ok) throw new Error();
|
|
|
|
document.cookie = `${ACCESS_TOKEN_COOKIE_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
|
|
if (menuCheckbox) menuCheckbox.checked = false;
|
|
|
|
window.location.reload();
|
|
} catch {
|
|
showToast('Unable to logout, please try again later.', 'error');
|
|
}
|
|
};
|
|
|
|
const rememberToggleHandler = (ev) => {
|
|
if (ev.target.checked) {
|
|
localStorage.setItem(REMEMBER_LOGIN_STORAGE_KEY, 'true');
|
|
return;
|
|
}
|
|
|
|
localStorage.removeItem(REMEMBER_LOGIN_STORAGE_KEY);
|
|
};
|
|
|
|
const twtContentKeyupHandler = (ev) => {
|
|
const hasContent =
|
|
(twtControlsContentInput.value?.trim() ?? '').length !== 0;
|
|
|
|
if (twtControlsContentInput) {
|
|
requestAnimationFrame(() => {
|
|
// Sync with browser repaint
|
|
twtControlsContentInput.style.height = 'auto';
|
|
twtControlsContentInput.style.height = `${Math.max(twtControlsContentInput.scrollHeight, 80)}px`;
|
|
});
|
|
}
|
|
|
|
if (isEditing || !hasContent)
|
|
twtSubmitButton.setAttribute('disabled', 'disabled');
|
|
else twtSubmitButton.removeAttribute('disabled');
|
|
|
|
if (hasContent && ev?.key === 'Enter' && ev?.ctrlKey)
|
|
twtFormSubmitHandler();
|
|
};
|
|
|
|
const twtFormSubmitHandler = async (ev, secondAttempt = false) => {
|
|
ev?.preventDefault();
|
|
|
|
try {
|
|
const twtData = new FormData(twtForm);
|
|
const twtContent = twtData.get('content').trim();
|
|
|
|
debug('twtForm submit data', { twtData });
|
|
if (!twtContent) return;
|
|
|
|
twtData.set('content', twtContent.replaceAll('\n', '\u2028'));
|
|
const twtBody = new URLSearchParams(twtData);
|
|
debug('twtForm submit body', { twtBody });
|
|
|
|
const res = await fetch(TWTXT_FILE_URL, {
|
|
method: 'POST',
|
|
body: twtBody,
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!res.ok && !secondAttempt) {
|
|
debug('twtForm submit - not OK, trying refresh');
|
|
await refreshToken();
|
|
return twtFormSubmitHandler(ev, true);
|
|
}
|
|
|
|
showToast('Twt sent');
|
|
|
|
twtForm.reset();
|
|
await loadTwtxtFile();
|
|
|
|
// scroll to bottom of file to show update
|
|
fileContentsSection.scrollTop = fileBox.scrollHeight;
|
|
} catch (err) {
|
|
debug('twtForm submit - error, trying refresh', err, !secondAttempt);
|
|
if (!secondAttempt) {
|
|
await refreshToken();
|
|
return twtFormSubmitHandler(ev, true);
|
|
}
|
|
|
|
showToast('Unable to twt, please try again later.', 'error');
|
|
console.error('Error POSTing twt', err);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const uploadChangeHandler = (ev) => {
|
|
uploadFiles(ev.target.files, ev.target.getAttribute('data-route'));
|
|
};
|
|
|
|
/* Attach Handlers to Listeners */
|
|
|
|
Array.from(uploadInputs).forEach((uploadInput) => {
|
|
uploadInput.addEventListener('change', uploadChangeHandler);
|
|
});
|
|
|
|
loginForm.addEventListener('submit', loginFormSubmitHandler);
|
|
|
|
twtForm.addEventListener('submit', twtFormSubmitHandler);
|
|
|
|
twtForm.addEventListener('keyup', twtContentKeyupHandler);
|
|
|
|
twtControlsContentInput.addEventListener('drop', dropHandler);
|
|
|
|
twtControlsContentInput.addEventListener('dragover', dragOverHandler);
|
|
|
|
twtLogoutButton.addEventListener('click', logoutHandler);
|
|
|
|
twtFileEditButton.addEventListener('click', editClickHandler);
|
|
|
|
twtxtEditForm.addEventListener('reset', editResetHandler);
|
|
|
|
twtxtEditForm.addEventListener('submit', editSubmitHandler);
|
|
|
|
window.addEventListener('dragover', dragOverWindowHandler);
|
|
|
|
window.addEventListener('drop', dropWindowHandler);
|
|
|
|
rememberToggle.addEventListener('change', rememberToggleHandler);
|
|
|
|
/* Start App*/
|
|
|
|
loadTwtxtFile().catch(() => {});
|
|
|
|
token = getCookie(ACCESS_TOKEN_COOKIE_KEY);
|
|
if (token) document.body.classList.add('js-authorized');
|
|
|
|
debug('client loaded');
|
|
})();
|