656 lines
22 KiB
TypeScript
656 lines
22 KiB
TypeScript
// @ts-ignore
|
|
const {$, $a, doAfterLoad, footer, header, nav} = window.fwdekker;
|
|
|
|
import {csrfToken, emptyFunction, getApi, postApi, ServerResponse, sharedMessageElement} from "./API";
|
|
import {CustomEventHandler} from "./CustomEventHandler";
|
|
import {clearMessage, clearMessages, showError, showInfo, showSuccess, showWarning} from "./Message";
|
|
|
|
|
|
/**
|
|
* Handles listeners to be invoked when the user logs in.
|
|
*/
|
|
const loginHandler = new CustomEventHandler();
|
|
/**
|
|
* Handles listeners to be invoked when the user logs out.
|
|
*/
|
|
const logoutHandler = new CustomEventHandler();
|
|
|
|
|
|
/**
|
|
* Refreshes the list of trackings in the table.
|
|
*/
|
|
function refreshTrackings(): void {
|
|
getApi(
|
|
{action: "list-trackings", token: csrfToken ?? ""},
|
|
sharedMessageElement,
|
|
(response) => {
|
|
// Remove old rows
|
|
$a("#trackings tbody tr").forEach((it: HTMLTableRowElement) => it.remove());
|
|
|
|
// Insert new rows
|
|
const tableBody = $("#trackings tbody");
|
|
response.payload.forEach((tracking: any) => {
|
|
const row = document.createElement("tr");
|
|
|
|
const nameCell = document.createElement("td");
|
|
const nameLink = document.createElement("a");
|
|
nameLink.href = "https://en.wikipedia.org/wiki/" + tracking.name;
|
|
nameLink.innerText = tracking.name;
|
|
if (tracking.deleted)
|
|
nameLink.classList.add("redLink");
|
|
nameCell.append(nameLink);
|
|
row.append(nameCell);
|
|
|
|
const statusCell = document.createElement("td");
|
|
let statusText;
|
|
if (tracking.deleted) {
|
|
statusText = "article not found";
|
|
} else {
|
|
switch (tracking.name) {
|
|
case "Adolf Hitler":
|
|
statusText = "dead 🥳";
|
|
break;
|
|
case "Vladimir Putin":
|
|
statusText = tracking.status === "alive" ? "alive ☹️" : "dead 🥳";
|
|
break;
|
|
default:
|
|
statusText = tracking.status;
|
|
break;
|
|
}
|
|
}
|
|
statusCell.innerText = statusText;
|
|
row.append(statusCell);
|
|
|
|
const deleteCell = document.createElement("td");
|
|
const deleteForm = document.createElement("form");
|
|
deleteForm.addEventListener("submit", (event: SubmitEvent) => {
|
|
event.preventDefault();
|
|
|
|
if (!window.confirm(`Are you sure you want to stop tracking ${tracking.name}?`)) return;
|
|
|
|
postApi(
|
|
{action: "remove-tracking", token: csrfToken, person_name: tracking.name},
|
|
deleteForm,
|
|
() => {
|
|
refreshTrackings();
|
|
showSuccess(
|
|
$("#removeTrackingValidationInfo"),
|
|
`Successfully removed <b>${tracking.name}</b>.`
|
|
);
|
|
}
|
|
)
|
|
});
|
|
const deleteButton = document.createElement("button");
|
|
deleteButton.innerText = "remove";
|
|
deleteForm.append(deleteButton);
|
|
deleteCell.append(deleteForm);
|
|
row.append(deleteCell);
|
|
|
|
tableBody.appendChild(row);
|
|
});
|
|
if (response.payload.length === 0) {
|
|
const row = document.createElement("tr");
|
|
row.classList.add("placeholder");
|
|
const cell = document.createElement("td");
|
|
cell.colSpan = 3;
|
|
cell.innerText = "You haven't added any articles to track yet.";
|
|
row.appendChild(cell);
|
|
tableBody.appendChild(row);
|
|
}
|
|
|
|
// Scroll to top, re-apply filter
|
|
$("#trackingsWrapper").scrollTop = 0;
|
|
$("#filterTrackingsQuery").dispatchEvent(new InputEvent("input"));
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Refreshes displays of the user's data.
|
|
*/
|
|
function refreshUserData(): void {
|
|
getApi(
|
|
{action: "get-user-data", token: csrfToken ?? ""},
|
|
sharedMessageElement,
|
|
(response) => {
|
|
const userData = response.payload;
|
|
|
|
// Account deletion
|
|
$("#deleteEmail").value = userData.email;
|
|
|
|
// Email
|
|
$("#emailCurrent").innerText = userData.email;
|
|
$("#emailVerified").innerText = userData.email_verified ? "yes" : "no";
|
|
if (!userData.email_verified)
|
|
showWarning(
|
|
sharedMessageElement,
|
|
"You will not receive any email notifications until you verify your email address. " +
|
|
"Check your inbox for further instructions."
|
|
);
|
|
else
|
|
clearMessage(sharedMessageElement);
|
|
$("#resendEmailVerificationButton").classList.toggle("hidden", userData.email_verified);
|
|
|
|
// Notifications
|
|
const toggleButton = $("#toggleNotificationsButton");
|
|
const notificationsEnabled = userData.email_verified && userData.email_notifications_enabled;
|
|
$("#notificationsEnabled").innerText = notificationsEnabled ? "enabled" : "disabled";
|
|
toggleButton.innerText = notificationsEnabled ? "disable" : "enable";
|
|
toggleButton.classList.toggle("hidden", !userData.email_verified);
|
|
|
|
// Password update time
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0)
|
|
const updateTime = new Date(userData.password_last_change * 1000);
|
|
updateTime.setHours(0, 0, 0, 0);
|
|
const diff = (+today - +updateTime) / 86400000;
|
|
$("#passwordLastChanged").innerText = diff === 0 ? "today" : diff + " days ago";
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Handles the action specified in the URL's get parameters.
|
|
*
|
|
* @param params the URL's get parameters
|
|
*/
|
|
function handleAction(params: URLSearchParams): void {
|
|
const sharedHomeLink = $("#sharedHomeLink");
|
|
let params_are_valid = true;
|
|
|
|
switch (params.get("action")) {
|
|
case "verify-email":
|
|
if (!params.has("email") || !params.has("token")) {
|
|
params_are_valid = false;
|
|
break;
|
|
}
|
|
|
|
postApi(
|
|
{
|
|
action: "verify-email",
|
|
token: csrfToken,
|
|
email: params.get("email"),
|
|
verify_token: params.get("token"),
|
|
},
|
|
sharedMessageElement,
|
|
() => {
|
|
sharedHomeLink.classList.remove("hidden");
|
|
redirectWithTimeout(
|
|
"./", 3, (secondsLeft) => {
|
|
showSuccess(
|
|
sharedMessageElement,
|
|
`Your email address has been verified. ` +
|
|
`You will be redirected after ${secondsLeft} second(s).`
|
|
);
|
|
});
|
|
},
|
|
() => sharedHomeLink.classList.remove("hidden")
|
|
);
|
|
break;
|
|
case "reset-password":
|
|
if (!params.has("email") || !params.has("token")) {
|
|
params_are_valid = false;
|
|
break;
|
|
}
|
|
|
|
getApi(
|
|
{
|
|
action: "validate-password-reset-token",
|
|
token: csrfToken ?? "",
|
|
email: params.get("email") ?? "",
|
|
reset_token: params.get("token") ?? "",
|
|
},
|
|
sharedMessageElement,
|
|
() => {
|
|
$("#resetPasswordToken").value = params.get("token");
|
|
$("#resetPasswordEmail").value = params.get("email");
|
|
$("#resetPasswordRow").classList.remove("hidden");
|
|
$("#resetPasswordPassword").focus();
|
|
},
|
|
() => sharedHomeLink.classList.remove("hidden")
|
|
);
|
|
break;
|
|
case null:
|
|
break;
|
|
default:
|
|
params_are_valid = false;
|
|
break;
|
|
}
|
|
|
|
if (!params_are_valid) {
|
|
sharedHomeLink.classList.remove("hidden");
|
|
showError(
|
|
sharedMessageElement,
|
|
`Invalid URL.`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Redirects the user to `target` after `seconds` seconds, calling `doEachSecond` after every second.
|
|
*
|
|
* @param target the location to redirect the user to after the timeout
|
|
* @param seconds the number of seconds before redirecting the user
|
|
* @param doEachSecond the function to invoke each second; the only argument is the number of seconds left at that time
|
|
*/
|
|
function redirectWithTimeout(target: string, seconds: number, doEachSecond: (secondsLeft: number) => void): void {
|
|
let secondsLeft = seconds;
|
|
const update = () => {
|
|
doEachSecond(secondsLeft);
|
|
secondsLeft--;
|
|
setTimeout(update, 1000);
|
|
};
|
|
update();
|
|
|
|
setTimeout(() => window.location.href = target, seconds * 1000);
|
|
}
|
|
|
|
|
|
// Initialize template
|
|
doAfterLoad(() => {
|
|
$("#nav").appendChild(nav("/Tools/Death-Notifier/"));
|
|
$("#header").appendChild(header({
|
|
title: "Death Notifier",
|
|
description: "Get notified when a famous person dies"
|
|
}));
|
|
$("#footer").appendChild(footer({
|
|
vcsURL: "https://git.fwdekker.com/tools/death-notifier/",
|
|
version: "v%%VERSION_NUMBER%%"
|
|
}));
|
|
$("main").classList.remove("hidden");
|
|
});
|
|
|
|
// Register event handlers
|
|
doAfterLoad(() => {
|
|
// Switch between logged-out and logged-in views
|
|
loginHandler.addListener(() => {
|
|
clearMessage(sharedMessageElement);
|
|
|
|
refreshUserData();
|
|
refreshTrackings();
|
|
|
|
$("#loginRow").classList.add("hidden")
|
|
$("#trackingRow").classList.remove("hidden");
|
|
$("#accountRow").classList.remove("hidden");
|
|
});
|
|
logoutHandler.addListener(() => {
|
|
clearMessage(sharedMessageElement);
|
|
|
|
$("#loginRow").classList.remove("hidden")
|
|
$("#trackingRow").classList.add("hidden");
|
|
$("#accountRow").classList.add("hidden");
|
|
$("#loginEmail").focus();
|
|
});
|
|
|
|
// Password visibility toggling
|
|
$a(".passwordToggle").forEach((toggle: HTMLElement) => {
|
|
const passwordField = $(`#${toggle.dataset.toggles}`);
|
|
|
|
const setState = (showPassword: boolean) => {
|
|
toggle.innerText = showPassword ? "Hide" : "Show";
|
|
passwordField.type = showPassword ? "text" : "password";
|
|
};
|
|
|
|
passwordField.form.addEventListener("reset", () => setState(false));
|
|
toggle.addEventListener("click", () => setState(passwordField.type === "password"));
|
|
});
|
|
|
|
// Message closing
|
|
$a(".formValidationInfo .closeButton").forEach((button: HTMLElement) => {
|
|
const parent = button.parentElement;
|
|
if (parent == null) return;
|
|
|
|
button.addEventListener("click", () => clearMessage(parent));
|
|
});
|
|
|
|
|
|
// Login
|
|
const loginForm = $("#loginForm");
|
|
loginForm.addEventListener("submit", (event: SubmitEvent) => {
|
|
event.preventDefault();
|
|
|
|
postApi(
|
|
{
|
|
action: "login",
|
|
token: csrfToken,
|
|
email: $("#loginEmail").value,
|
|
password: $("#loginPassword").value,
|
|
},
|
|
event.target as HTMLFormElement,
|
|
() => loginHandler.invokeListeners()
|
|
);
|
|
});
|
|
loginHandler.addListener(() => {
|
|
loginForm.reset();
|
|
clearMessages(loginForm);
|
|
});
|
|
|
|
const registerForm = $("#registerForm");
|
|
registerForm.addEventListener("submit", (event: SubmitEvent) => {
|
|
event.preventDefault();
|
|
|
|
postApi(
|
|
{
|
|
action: "register",
|
|
token: csrfToken,
|
|
email: $("#registerEmail").value,
|
|
password: $("#registerPassword").value
|
|
},
|
|
registerForm,
|
|
() => {
|
|
// TODO: Add client-side form validation
|
|
registerForm.reset();
|
|
showSuccess(
|
|
$("#registerFormValidationInfo"),
|
|
"Account created successfully! You may now log in."
|
|
);
|
|
$("#loginEmail").focus();
|
|
}
|
|
);
|
|
});
|
|
loginHandler.addListener(() => {
|
|
registerForm.reset();
|
|
clearMessages(registerForm);
|
|
});
|
|
|
|
const logoutForm = $("#logoutForm");
|
|
logoutForm.addEventListener("submit", (event: SubmitEvent) => {
|
|
event.preventDefault();
|
|
|
|
postApi(
|
|
{
|
|
action: "logout",
|
|
token: csrfToken,
|
|
},
|
|
logoutForm,
|
|
() => {
|
|
logoutForm.reset();
|
|
logoutHandler.invokeListeners();
|
|
}
|
|
);
|
|
});
|
|
|
|
|
|
// Forgot password
|
|
const sendPasswordResetForm = $("#sendPasswordResetForm");
|
|
sendPasswordResetForm.addEventListener("submit", (event: SubmitEvent) => {
|
|
event.preventDefault();
|
|
|
|
postApi(
|
|
{
|
|
action: "send-password-reset",
|
|
token: csrfToken,
|
|
email: $("#sendPasswordResetEmail").value,
|
|
},
|
|
sendPasswordResetForm,
|
|
() => {
|
|
sendPasswordResetForm.reset();
|
|
showSuccess(
|
|
$("#sendPasswordResetFormValidationInfo"),
|
|
"Password reset email sent successfully!"
|
|
);
|
|
}
|
|
);
|
|
});
|
|
loginHandler.addListener(() => {
|
|
sendPasswordResetForm.reset();
|
|
clearMessages(sendPasswordResetForm);
|
|
});
|
|
|
|
const resetPasswordForm = $("#resetPasswordForm");
|
|
resetPasswordForm.addEventListener("submit", (event: SubmitEvent) => {
|
|
event.preventDefault();
|
|
|
|
postApi(
|
|
{
|
|
action: "reset-password",
|
|
token: csrfToken,
|
|
email: $("#resetPasswordEmail").value,
|
|
reset_token: $("#resetPasswordToken").value,
|
|
password: $("#resetPasswordPassword").value,
|
|
},
|
|
resetPasswordForm,
|
|
() => {
|
|
$("#resetPasswordForm").reset();
|
|
redirectWithTimeout(
|
|
"./", 3, (secondsLeft) => {
|
|
showSuccess(
|
|
$("#resetPasswordFormValidationInfo"),
|
|
`Your password has been updated. You will be redirected after ${secondsLeft} second(s).`
|
|
);
|
|
}
|
|
)
|
|
}
|
|
);
|
|
});
|
|
|
|
$("#forgotPasswordGoTo").addEventListener("click", (event: MouseEvent) => {
|
|
event.preventDefault();
|
|
|
|
$("#loginRow").classList.add("hidden");
|
|
$("#sendForgotPasswordRow").classList.remove("hidden");
|
|
|
|
const resetEmail = $("#sendPasswordResetEmail");
|
|
resetEmail.value = $("#loginEmail").value;
|
|
resetEmail.focus();
|
|
});
|
|
$("#forgotPasswordGoBack").addEventListener("click", (event: MouseEvent) => {
|
|
event.preventDefault();
|
|
|
|
$("#sendPasswordResetForm").reset();
|
|
$("#sendForgotPasswordRow").classList.add("hidden");
|
|
$("#loginRow").classList.remove("hidden");
|
|
|
|
const loginEmail = $("#loginEmail");
|
|
loginEmail.value = $("#sendPasswordResetEmail").value;
|
|
loginEmail.focus();
|
|
});
|
|
|
|
|
|
// Account management
|
|
const deleteForm = $("#deleteForm");
|
|
deleteForm.addEventListener("submit", (event: SubmitEvent) => {
|
|
event.preventDefault();
|
|
|
|
const actual_email = $("#deleteEmail").value;
|
|
const entered_email = window.prompt(
|
|
`Are you sure you want to delete your account? ` +
|
|
`This action cannot be undone. ` +
|
|
`Enter ${actual_email} to confirm.`
|
|
);
|
|
if (entered_email === null) {
|
|
return;
|
|
} else if (entered_email !== actual_email) {
|
|
showError($("#deleteFormValidationInfo"), "Incorrect email address.");
|
|
return;
|
|
}
|
|
|
|
postApi(
|
|
{action: "user-delete", token: csrfToken},
|
|
deleteForm,
|
|
() => {
|
|
logoutHandler.invokeListeners();
|
|
showSuccess(sharedMessageElement, "Your account has been deleted.");
|
|
}
|
|
);
|
|
});
|
|
logoutHandler.addListener(() => clearMessages(deleteForm));
|
|
|
|
const resendEmailVerificationForm = $("#resendEmailVerificationForm");
|
|
resendEmailVerificationForm.addEventListener("submit", (event: SubmitEvent) => {
|
|
event.preventDefault();
|
|
|
|
postApi(
|
|
{action: "resend-verify-email", token: csrfToken},
|
|
resendEmailVerificationForm,
|
|
() => {
|
|
refreshUserData();
|
|
showSuccess(
|
|
$("#resetEmailVerificationFormValidationInfo"),
|
|
"Email verification resent successfully!"
|
|
);
|
|
}
|
|
);
|
|
});
|
|
logoutHandler.addListener(() => clearMessages(resendEmailVerificationForm));
|
|
|
|
const toggleNotificationsForm = $("#toggleNotificationsForm");
|
|
toggleNotificationsForm.addEventListener("submit", (event: SubmitEvent) => {
|
|
event.preventDefault();
|
|
|
|
postApi(
|
|
{action: "toggle-notifications", token: csrfToken},
|
|
toggleNotificationsForm,
|
|
() => refreshUserData()
|
|
);
|
|
});
|
|
logoutHandler.addListener(() => clearMessages(toggleNotificationsForm));
|
|
|
|
const updateEmailForm = $("#updateEmailForm");
|
|
updateEmailForm.addEventListener("submit", (event: SubmitEvent) => {
|
|
event.preventDefault();
|
|
|
|
postApi(
|
|
{
|
|
action: "update-email",
|
|
token: csrfToken,
|
|
email: $("#updateEmailEmail").value,
|
|
},
|
|
updateEmailForm,
|
|
() => {
|
|
updateEmailForm.reset();
|
|
refreshUserData();
|
|
showSuccess(
|
|
$("#updateEmailFormValidationInfo"),
|
|
"Email address updated successfully! " +
|
|
"Check your inbox for the verification email. " +
|
|
"You will not receive notifications until you verify your email address."
|
|
);
|
|
}
|
|
);
|
|
});
|
|
logoutHandler.addListener(() => {
|
|
updateEmailForm.reset();
|
|
clearMessages(updateEmailForm);
|
|
});
|
|
|
|
const updatePasswordForm = $("#updatePasswordForm");
|
|
updatePasswordForm.addEventListener("submit", (event: SubmitEvent) => {
|
|
event.preventDefault();
|
|
|
|
postApi(
|
|
{
|
|
action: "update-password",
|
|
token: csrfToken,
|
|
password_old: $("#updatePasswordPasswordOld").value,
|
|
password_new: $("#updatePasswordPasswordNew").value,
|
|
},
|
|
event.target as HTMLFormElement,
|
|
() => {
|
|
updatePasswordForm.reset();
|
|
refreshUserData();
|
|
showSuccess($("#updatePasswordFormValidationInfo"), "Password updated successfully!");
|
|
}
|
|
);
|
|
});
|
|
logoutHandler.addListener(() => {
|
|
updatePasswordForm.reset();
|
|
clearMessages(updatePasswordForm);
|
|
});
|
|
|
|
|
|
// Tracking management
|
|
const queryInput = $("#filterTrackingsQuery");
|
|
$("#filterTrackingsForm").addEventListener("submit", (event: SubmitEvent) => event.preventDefault());
|
|
queryInput.addEventListener("input", (event: InputEvent) => {
|
|
event.preventDefault();
|
|
|
|
$("#trackingsNoMatches")?.remove();
|
|
|
|
const queryWords = $("#filterTrackingsQuery").value.trim().toLowerCase().split(" ");
|
|
let foundMatches = false;
|
|
$a("#trackings tbody tr").forEach((row: HTMLTableRowElement) => {
|
|
const rowText = row.innerText.toLowerCase();
|
|
const matches = row.classList.contains("placeholder")
|
|
? (queryWords.length === 1 && queryWords[0] === "")
|
|
: queryWords.every((word: string) => rowText.includes(word));
|
|
foundMatches = foundMatches || matches;
|
|
row.classList.toggle("hidden", !matches);
|
|
});
|
|
|
|
if (!foundMatches) {
|
|
const row = document.createElement("tr");
|
|
row.id = "trackingsNoMatches";
|
|
row.classList.add("placeholder");
|
|
const cell = document.createElement("td");
|
|
cell.colSpan = 3;
|
|
cell.innerText = "No articles match your query.";
|
|
row.appendChild(cell);
|
|
$("#trackings tbody").appendChild(row);
|
|
}
|
|
});
|
|
$("#filterTrackingsClear").addEventListener("click", () => {
|
|
queryInput.value = "";
|
|
queryInput.dispatchEvent(new InputEvent("input"));
|
|
});
|
|
|
|
const addTrackingForm = $("#addTrackingForm");
|
|
addTrackingForm.addEventListener("submit", (event: SubmitEvent) => {
|
|
event.preventDefault();
|
|
|
|
postApi(
|
|
{
|
|
action: "add-tracking",
|
|
token: csrfToken,
|
|
person_name: $("#addTrackingPersonName").value,
|
|
},
|
|
addTrackingForm,
|
|
(response: ServerResponse) => {
|
|
addTrackingForm.reset();
|
|
refreshTrackings();
|
|
|
|
showSuccess(
|
|
$("#addTrackingFormValidationInfo"),
|
|
response.payload["renamed"]
|
|
? (
|
|
`Successfully added <b>${response.payload["input"]}</b> as ` +
|
|
`<b>${response.payload["name"]}</b>!`
|
|
)
|
|
: `Successfully added <b>${response.payload["name"]}</b>!`
|
|
);
|
|
}
|
|
);
|
|
});
|
|
logoutHandler.addListener(() => {
|
|
addTrackingForm.reset();
|
|
clearMessages(addTrackingForm);
|
|
});
|
|
});
|
|
|
|
// Run initialization code
|
|
doAfterLoad(() => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
getApi(
|
|
{action: "start-session"},
|
|
sharedMessageElement,
|
|
(response: ServerResponse) => {
|
|
if (!params.has("action")) {
|
|
if (response.payload["logged_in"] === true)
|
|
loginHandler.invokeListeners();
|
|
else
|
|
logoutHandler.invokeListeners();
|
|
}
|
|
},
|
|
emptyFunction,
|
|
emptyFunction,
|
|
(response) => {
|
|
// Always execute the following
|
|
const message = (response?.payload ?? {})["global_message"];
|
|
if (message != null)
|
|
showInfo($("#globalMessage"), message);
|
|
|
|
handleAction(params);
|
|
}
|
|
);
|
|
});
|