alpha release
update v0.8.0
This commit is contained in:
506
dist/src/client/script.js
vendored
Normal file
506
dist/src/client/script.js
vendored
Normal file
@@ -0,0 +1,506 @@
|
||||
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');
|
||||
})();
|
||||
Reference in New Issue
Block a user