death-notifier/src/main/js/Main.ts

481 lines
16 KiB
TypeScript

// @ts-ignore
const {$, $a, doAfterLoad, footer, header, nav} = window.fwdekker;
import {csrfToken, getApi, postApi, sharedMessageElement} from "./API";
import {CustomEventHandler} from "./CustomEventHandler";
import {clearMessages, showError, 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"},
sharedMessageElement,
(response) => {
// Remove old rows
$a("#trackings tbody tr:not(:last-child)").forEach((it: HTMLTableRowElement) => it.remove());
// Insert new rows
const tableBody = $("#trackings tbody");
const lastRow = $("#trackings tbody tr:last-child");
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");
statusCell.innerText =
tracking.deleted
? "page not found"
: (tracking.name === "Adolf Hitler" ? "deceased 🥳" : tracking.status);
row.append(statusCell);
const deleteCell = document.createElement("td");
const deleteForm = document.createElement("form");
deleteForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{action: "remove-tracking", token: csrfToken, person_name: tracking.name},
deleteForm,
() => refreshTrackings()
)
});
const deleteButton = document.createElement("button");
deleteButton.innerText = "remove";
deleteForm.append(deleteButton);
deleteCell.append(deleteForm);
row.append(deleteCell);
tableBody.insertBefore(row, lastRow);
});
}
);
}
/**
* Refreshes displays of the user's data.
*/
function refreshUserData(): void {
getApi(
{action: "get-user-data"},
sharedMessageElement,
(response) => {
const userData = response.payload;
// 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."
);
$("#resendEmailVerificationButton").classList.remove("hidden");
}
// 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";
}
)
}
/**
* 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(() => {
loginHandler.addListener(() => {
refreshUserData();
refreshTrackings();
$("#loginRow").classList.add("hidden")
$("#trackingRow").classList.remove("hidden");
$("#accountRow").classList.remove("hidden");
});
logoutHandler.addListener(() => {
$("#loginRow").classList.remove("hidden")
$("#trackingRow").classList.add("hidden");
$("#accountRow").classList.add("hidden");
});
// 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,
password_confirm: $("#registerPasswordConfirm").value,
},
registerForm,
() => {
// TODO: Add client-side form validation
registerForm.reset();
showSuccess(
$(".formValidationInfo", registerForm),
"Account created successfully! You may now log in."
);
}
);
});
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(
$(".formValidationInfo", sendPasswordResetForm),
"Password reset email sent successfully!"
);
}
);
});
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,
password_confirm: $("#resetPasswordPasswordConfirm").value,
},
resetPasswordForm,
() => {
redirectWithTimeout(
"./", 3, (secondsLeft) => {
showSuccess(
$(".formValidationInfo", resetPasswordForm),
`Your password has been updated. You will be redirected after ${secondsLeft} seconds.`
);
}
)
}
);
});
$("#forgotPasswordGoTo").addEventListener("click", (event: MouseEvent) => {
event.preventDefault();
$("#sendPasswordResetEmail").value = $("#loginEmail").value;
$("#loginRow").classList.add("hidden");
$("#sendForgotPasswordRow").classList.remove("hidden");
});
$("#forgotPasswordGoBack").addEventListener("click", (event: MouseEvent) => {
event.preventDefault();
$("#loginEmail").value = $("#sendPasswordResetEmail").value;
$("#sendPasswordResetForm").reset();
$("#sendForgotPasswordRow").classList.add("hidden");
$("#loginRow").classList.remove("hidden");
});
// Account management
const updateEmailForm = $("#updateEmailForm");
updateEmailForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "update-email",
token: csrfToken,
email: $("#updateEmailEmail").value,
},
updateEmailForm,
() => {
updateEmailForm.reset();
refreshUserData();
showSuccess(
$(".formValidationInfo", updateEmailForm),
"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 resendEmailVerificationForm = $("#resendEmailVerificationForm");
resendEmailVerificationForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{action: "resend-verify-email", token: csrfToken},
resendEmailVerificationForm,
() => {
resendEmailVerificationForm.reset();
refreshUserData();
showSuccess(
$(".formValidationInfo", resendEmailVerificationForm),
"Email verification resent successfully!"
);
}
);
});
logoutHandler.addListener(() => {
resendEmailVerificationForm.reset();
clearMessages(resendEmailVerificationForm);
});
const updatePasswordForm = $("#updatePasswordForm");
updatePasswordForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "update-password",
token: csrfToken,
password_old: $("#updatePasswordPasswordOld").value,
password_new: $("#updatePasswordPasswordNew").value,
password_confirm: $("#updatePasswordPasswordConfirm").value,
},
event.target as HTMLFormElement,
() => {
updatePasswordForm.reset();
refreshUserData();
showSuccess($(".formValidationInfo", updatePasswordForm), "Password updated successfully!");
}
);
});
logoutHandler.addListener(() => {
updatePasswordForm.reset();
clearMessages(updatePasswordForm);
});
// Tracking management
const addTrackingForm = $("#addTrackingForm");
addTrackingForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
postApi(
{
action: "add-tracking",
token: csrfToken,
person_name: $("#addTrackingPersonName").value,
},
addTrackingForm,
() => {
addTrackingForm.reset();
refreshTrackings();
}
);
});
logoutHandler.addListener(() => {
addTrackingForm.reset();
clearMessages(addTrackingForm);
});
});
// Run initialization code
doAfterLoad(() => {
const get_params = new URLSearchParams(window.location.search);
getApi(
{action: "start-session"},
sharedMessageElement,
() => {
if (!get_params.has("action"))
loginHandler.invokeListeners();
},
() => {
if (!get_params.has("action"))
logoutHandler.invokeListeners();
},
() => {
// Do nothing
},
() => {
let valid_action_params = true;
switch (get_params.get("action")) {
case "verify-email":
if (get_params.has("email") && get_params.has("token")) {
postApi(
{
action: "verify-email",
token: csrfToken,
email: get_params.get("email"),
verify_token: get_params.get("token"),
},
sharedMessageElement,
() => {
redirectWithTimeout(
"./", 3, (secondsLeft) => {
showSuccess(
sharedMessageElement,
`Your email address has been verified. ` +
`You will be redirected after ${secondsLeft} seconds.`
);
});
},
() => $("#sharedHomeLink").classList.remove("hidden")
);
} else {
valid_action_params = false;
}
break;
case "reset-password":
if (get_params.has("email") && get_params.has("token")) {
getApi(
{
action: "validate-password-reset-token",
token: csrfToken ?? "",
email: get_params.get("email") ?? "",
reset_token: get_params.get("token") ?? "",
},
sharedMessageElement,
() => {
$("#resetPasswordEmail").value = get_params.get("email");
$("#resetPasswordToken").value = get_params.get("token");
$("#resetPasswordRow").classList.remove("hidden");
}
);
} else {
valid_action_params = false;
}
break;
case null:
break;
default:
valid_action_params = false;
break;
}
if (!valid_action_params) {
redirectWithTimeout(
"./", 3, (secondsLeft) => {
showError(
sharedMessageElement,
`Invalid URL. You will be redirected after ${secondsLeft} seconds.`
);
}
);
}
}
);
});