Migrate to template v3
This commit is contained in:
parent
8fe82f4a18
commit
69972cd3f1
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "death-notifier",
|
||||
"version": "0.14.5", "_comment_version": "Also update version in `composer.json`!",
|
||||
"version": "0.14.6", "_comment_version": "Also update version in `composer.json`!",
|
||||
"description": "Get notified when a famous person dies.",
|
||||
"author": "Florine W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
|
|
@ -1,121 +1,23 @@
|
|||
:root {
|
||||
/* Colors taken from https://isabelcastillo.com/error-info-messages-css */
|
||||
--info-color: #00529b;
|
||||
--info-bg-color: #bde5f8;
|
||||
--success-color: #4f8a10;
|
||||
--success-bg-color: #dff2bf;
|
||||
--warning-color: #9f6000;
|
||||
--warning-bg-color: #feefb3;
|
||||
--error-color: #d8000c;
|
||||
--error-bg-color: #ffbaba;
|
||||
|
||||
/* Color taken from https://fandom.com */
|
||||
--fandom-redlink: #ba0000;
|
||||
}
|
||||
|
||||
|
||||
/* General definitions */
|
||||
a.redLink {
|
||||
a.red-link {
|
||||
color: var(--fandom-redlink);
|
||||
}
|
||||
|
||||
.linkButton {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
|
||||
/* Toggleable password */
|
||||
input.passwordToggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* Inline form */
|
||||
.inlineForm {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
|
||||
.inlineForm button {
|
||||
margin: 0;
|
||||
padding: .4rem;
|
||||
height: unset;
|
||||
line-height: unset;
|
||||
}
|
||||
|
||||
.inlineForm label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
||||
/* Input validation */
|
||||
.info {
|
||||
--message-color: var(--info-color);
|
||||
--message-bg-color: var(--info-bg-color);
|
||||
color: var(--message-color);
|
||||
}
|
||||
|
||||
.success {
|
||||
--message-color: var(--success-color);
|
||||
--message-bg-color: var(--success-bg-color);
|
||||
color: var(--message-color);
|
||||
}
|
||||
|
||||
.warning {
|
||||
--message-color: var(--warning-color);
|
||||
--message-bg-color: var(--warning-bg-color);
|
||||
color: var(--message-color);
|
||||
}
|
||||
|
||||
.error, .error:focus {
|
||||
--message-color: var(--error-color);
|
||||
--message-bg-color: var(--error-bg-color);
|
||||
color: var(--message-color);
|
||||
}
|
||||
|
||||
|
||||
.formValidationInfo {
|
||||
.flex-columns {
|
||||
display: flex;
|
||||
border: 1px solid var(--message-color);
|
||||
background-color: var(--message-bg-color);
|
||||
align-items: flex-start;
|
||||
/*noinspection CssUnresolvedCustomProperty*/
|
||||
grid-column-gap: calc(var(--block-spacing-horizontal) * 3);
|
||||
}
|
||||
|
||||
.formValidationInfo:not(.hasMessage) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.formValidationInfo .validationInfo {
|
||||
.flex-columns > * {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
padding: 0 2rem 0 2rem;
|
||||
margin: 0;
|
||||
|
||||
width: 5rem;
|
||||
height: unset;
|
||||
line-height: unset;
|
||||
border: 0;
|
||||
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.closeButton, .closeButton:active, .closeButton:focus, .closeButton:hover {
|
||||
background-color: unset;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.validationInfo.hasMessage, .inputHint {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label ~ .validationInfo, label ~ .inputHint {
|
||||
margin-top: -1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
input.hasMessage, input.hasMessage:focus {
|
||||
border-color: var(--message-color, inherit);
|
||||
}
|
||||
|
||||
|
||||
|
@ -124,33 +26,26 @@ input.hasMessage, input.hasMessage:focus {
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
#filterTrackingsForm, #filterTrackingsQuery {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
|
||||
#filterTrackingsQuery {
|
||||
#filter-trackings-query {
|
||||
width: unset;
|
||||
}
|
||||
|
||||
#trackingsWrapper {
|
||||
#trackings-wrapper {
|
||||
max-height: 54rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: var(--typography-spacing-vertical);
|
||||
|
||||
border-bottom: .1rem solid #e1e1e1;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#trackingsWrapper {
|
||||
/* Shows scrolling shadows. Adapted from https://css-tricks.com/books/greatest-css-tricks/scroll-shadows/ */
|
||||
background: linear-gradient(white 30%, rgba(255, 255, 255, 0)) no-repeat local center calc(2.4rem + 1.6em),
|
||||
background: linear-gradient(white 30%, rgba(255, 255, 255, 0)) no-repeat local center calc(var(--spacing) + 1.6em),
|
||||
linear-gradient(rgba(255, 255, 255, 0), white 70%) no-repeat local center bottom,
|
||||
radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) no-repeat scroll center calc(2.4rem + 1.6em),
|
||||
radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) no-repeat scroll center calc(var(--spacing) + 1.6em),
|
||||
radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) no-repeat scroll center bottom;
|
||||
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
|
||||
}
|
||||
|
||||
#trackings {
|
||||
display: table; /* Overrides Milligram. Enables sticky table header */
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
@ -165,24 +60,7 @@ input.hasMessage, input.hasMessage:focus {
|
|||
padding-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
#trackings tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#trackings form, #trackings button {
|
||||
#trackings form,
|
||||
#trackings button {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
|
||||
#addTrackingPersonName {
|
||||
width: unset;
|
||||
}
|
||||
|
||||
|
||||
/* Settings */
|
||||
#settingsRows input {
|
||||
width: unset;
|
||||
}
|
||||
|
||||
#settingsRows input[type="checkbox"] + label {
|
||||
display: inline;
|
||||
}
|
||||
|
|
|
@ -8,17 +8,23 @@
|
|||
<meta name="description" content="Get notified when a famous person dies." />
|
||||
<meta name="theme-color" content="#0033cc" />
|
||||
|
||||
<meta name="fwd:nav:target" content="#nav" />
|
||||
<meta name="fwd:nav:highlight-path" content="/Tools/Death-Notifier/" />
|
||||
<meta name="fwd:footer:target" content="#footer" />
|
||||
<meta name="fwd:footer:vcs-url" content="https://git.fwdekker.com/tools/death-notifier/" />
|
||||
<meta name="fwd:footer:version" content="%%VERSION_NUMBER%%" />
|
||||
<meta name="fwd:validation:load-forms" />
|
||||
|
||||
<title>Death Notifier | FWDekker</title>
|
||||
|
||||
<link rel="stylesheet" href="https://static.fwdekker.com/fonts/roboto/roboto.css" />
|
||||
<link rel="stylesheet" href="https://static.fwdekker.com/lib/template/2.x.x/template.css?v=%%VERSION_NUMBER%%" />
|
||||
<link rel="stylesheet" href="https://static.fwdekker.com/lib/template/3.x.x/template.css?v=%%VERSION_NUMBER%%" />
|
||||
<!--suppress HtmlUnknownTarget -->
|
||||
<link rel="stylesheet" href="main.css?v=%%VERSION_NUMBER%%" />
|
||||
<script async src="https://stats.fwdekker.com/count.js"
|
||||
data-goatcounter="https://stats.fwdekker.com/count"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<noscript class="fwd-js-notice">
|
||||
<img src="https://stats.fwdekker.com/count?p=/tools/death-notifier/" alt="Counting pixel" />
|
||||
|
||||
<p>
|
||||
|
@ -27,292 +33,367 @@
|
|||
<a href="https://www.enable-javascript.com/">instructions on how to enable JavaScript in your web browser</a>.
|
||||
</p>
|
||||
</noscript>
|
||||
<main class="hidden">
|
||||
<div id="nav"></div>
|
||||
<div id="contents">
|
||||
<div id="header"></div>
|
||||
<nav id="nav"></nav>
|
||||
<main class="container hidden">
|
||||
<div role="document">
|
||||
<section>
|
||||
<header class="fwd-header">
|
||||
<hgroup>
|
||||
<h1><a href=".">Death Notifier</a></h1>
|
||||
<h2>Get notified when a famous person dies.</h2>
|
||||
</hgroup>
|
||||
</header>
|
||||
|
||||
<section class="container">
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<p id="globalMessage" class="formValidationInfo">
|
||||
<output class="validationInfo" for="globalMessage"></output>
|
||||
</p>
|
||||
<p id="sharedValidationInfo" class="formValidationInfo">
|
||||
<output class="validationInfo" for="sharedValidationInfo"></output>
|
||||
<button type="button" class="closeButton">×</button>
|
||||
</p>
|
||||
<p id="sharedHomeLink" class="hidden">
|
||||
<a href="./">Click here to return to the main page</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<article id="global-message" class="status-card hidden">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
<article id="shared-status-card" class="status-card hidden">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
<p id="shared-home-link" class="hidden">
|
||||
<a href="./">Click here to return to the main page</a>
|
||||
</p>
|
||||
|
||||
<div class="row hidden" id="loginRow">
|
||||
<div class="column">
|
||||
<h2>Log in</h2>
|
||||
<p>Already have an account? Welcome back!</p>
|
||||
<form id="loginForm" novalidate>
|
||||
<p id="loginFormValidationInfo" class="formValidationInfo">
|
||||
<output class="validationInfo" for="loginFormValidationInfo"></output>
|
||||
<button type="button" class="closeButton">×</button>
|
||||
</p>
|
||||
<div id="welcome-part" class="flex-columns">
|
||||
<article>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h2>Log in</h2>
|
||||
<h3>Already have an account? Welcome back!</h3>
|
||||
</hgroup>
|
||||
</header>
|
||||
<div class="article-contents">
|
||||
<form id="login-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="login-form">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
|
||||
<label for="loginEmail">Email</label>
|
||||
<input id="loginEmail" type="email" name="email" autocomplete="on" />
|
||||
<output class="validationInfo" for="loginEmail"></output>
|
||||
<fieldset>
|
||||
<label for="login-email">Email</label>
|
||||
<input id="login-email" type="email" name="email" autocomplete="on" />
|
||||
<small id="login-email-hint" data-hint-for="login-email"></small>
|
||||
|
||||
<label for="loginPassword">Password</label>
|
||||
<div class="inputWithButton">
|
||||
<input id="loginPassword" type="password" name="password" />
|
||||
<button type="button" class="passwordToggle" data-toggles="loginPassword">Show</button>
|
||||
</div>
|
||||
<output class="validationInfo" for="loginPassword"></output>
|
||||
<label for="login-password">Password</label>
|
||||
<input id="login-password" type="password" name="password" />
|
||||
<small id="login-password-hint" data-hint-for="login-password"></small>
|
||||
|
||||
<button id="loginButton">Log in</button>
|
||||
<a id="forgotPasswordGoTo" class="linkButton" href="#">Forgot password?</a>
|
||||
</form>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h2>Register</h2>
|
||||
<p>
|
||||
New user?
|
||||
Create an account!
|
||||
You can always delete your account and associated data.
|
||||
Check the <a href="https://fwdekker.com/privacy/">privacy policy</a> for more information.
|
||||
</p>
|
||||
<form id="registerForm" novalidate>
|
||||
<p id="registerFormValidationInfo" class="formValidationInfo">
|
||||
<output class="validationInfo" for="registerFormValidationInfo"></output>
|
||||
<button type="button" class="closeButton">×</button>
|
||||
</p>
|
||||
<input type="checkbox" role="switch" id="login-password-toggle" class="password-toggle"
|
||||
data-toggles="login-password" />
|
||||
<label for="login-password-toggle">Show password</label>
|
||||
</fieldset>
|
||||
|
||||
<label for="registerEmail">Email</label>
|
||||
<input id="registerEmail" type="email" name="email" autocomplete="on" />
|
||||
<output class="validationInfo" for="registerEmail"></output>
|
||||
|
||||
<label for="registerPassword">Password</label>
|
||||
<div class="inputWithButton">
|
||||
<input id="registerPassword" type="password" name="password" />
|
||||
<button type="button" class="passwordToggle" data-toggles="registerPassword">Show</button>
|
||||
</div>
|
||||
<fieldset>
|
||||
<button id="login-submit">Log in</button>
|
||||
<a role="button" id="forgot-password-go-to" class="outline" href="#">
|
||||
Forgot password?
|
||||
</a>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<article>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h2>Register</h2>
|
||||
<h3>
|
||||
New user?
|
||||
Create an account!
|
||||
You can always delete your account and associated data.
|
||||
Check the <a href="https://fwdekker.com/privacy/">privacy policy</a> for more
|
||||
information.
|
||||
</h3>
|
||||
</hgroup>
|
||||
</header>
|
||||
<div class="article-contents">
|
||||
<!-- TODO: Receive requirements from server(?) (combine this with client-side validation) -->
|
||||
<span class="inputHint" data-for="registerPassword">Use at least 8 characters.</span>
|
||||
<output class="validationInfo" for="registerPassword"></output>
|
||||
<form id="register-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="register-form">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
|
||||
<button id="registerButton">Create account</button>
|
||||
</form>
|
||||
</div>
|
||||
<fieldset>
|
||||
<label for="register-email">Email</label>
|
||||
<input id="register-email" type="email" name="email" autocomplete="on" />
|
||||
<small id="register-email-hint" data-hint-for="register-email"></small>
|
||||
|
||||
<label for="register-password">Password</label>
|
||||
<input id="register-password" type="password" name="password" />
|
||||
<small id="register-password-hint" data-hint-for="register-password"
|
||||
data-hint="Use at least 8 characters."></small>
|
||||
|
||||
<input type="checkbox" role="switch" id="register-password-toggle"
|
||||
class="password-toggle"
|
||||
data-toggles="register-password" />
|
||||
<label for="register-password-toggle">Show password</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<button id="register-submit">Create account</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="row hidden" id="sendForgotPasswordRow">
|
||||
<div class="column column-50">
|
||||
<h2>Forgot password</h2>
|
||||
<p>Send an email to help reset your password.</p>
|
||||
<form id="sendPasswordResetForm" novalidate>
|
||||
<p id="sendPasswordResetFormValidationInfo" class="formValidationInfo">
|
||||
<output class="validationInfo" for="sendPasswordResetFormValidationInfo"></output>
|
||||
<button type="button" class="closeButton">×</button>
|
||||
</p>
|
||||
<div id="send-forgot-password-part" class="flex-columns hidden">
|
||||
<article>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h2>Forgot password</h2>
|
||||
<h3>Send an email to help reset your password.</h3>
|
||||
</hgroup>
|
||||
</header>
|
||||
<div class="article-contents">
|
||||
<form id="send-password-reset-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="send-password-reset-form">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
|
||||
<label for="sendPasswordResetEmail">Email</label>
|
||||
<input id="sendPasswordResetEmail" type="email" name="email" autocomplete="on" />
|
||||
<output class="validationInfo" for="sendPasswordResetEmail"></output>
|
||||
<fieldset>
|
||||
<label for="send-password-reset-email">Email</label>
|
||||
<input id="send-password-reset-email" type="email" name="email" autocomplete="on" />
|
||||
<small id="send-password-reset-email-hint"
|
||||
data-hint-for="send-password-reset-email"></small>
|
||||
</fieldset>
|
||||
|
||||
<button id="sendPasswordResetButton">Send email</button>
|
||||
<a id="forgotPasswordGoBack" class="linkButton" href="#">Return to log in form</a>
|
||||
</form>
|
||||
</div>
|
||||
<fieldset>
|
||||
<button id="send-password-reset-submit">Send email</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
<a role="button" id="forgot-password-go-back" class="outline" href="#">Return to log in form</a>
|
||||
</div>
|
||||
</article>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="row hidden" id="resetPasswordRow">
|
||||
<div class="column column-50">
|
||||
<h2>Reset password</h2>
|
||||
<p>Set a new password for your account.</p>
|
||||
<form id="resetPasswordForm" novalidate>
|
||||
<p id="resetPasswordFormValidationInfo" class="formValidationInfo">
|
||||
<output class="validationInfo" for="resetPasswordFormValidationInfo"></output>
|
||||
<button type="button" class="closeButton">×</button>
|
||||
</p>
|
||||
<div id="reset-password-part" class="flex-columns hidden">
|
||||
<article>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h2>Reset password</h2>
|
||||
<h3>Set a new password for your account.</h3>
|
||||
</hgroup>
|
||||
</header>
|
||||
|
||||
<input id="resetPasswordToken" type="hidden" name="token" />
|
||||
<form id="reset-password-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="reset-password-form">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
|
||||
<label for="resetPasswordEmail">Email</label>
|
||||
<input id="resetPasswordEmail" type="email" name="email" disabled />
|
||||
<output class="validationInfo" for="resetPasswordEmail"></output>
|
||||
<fieldset>
|
||||
<input id="reset-password-token" type="hidden" name="token" />
|
||||
|
||||
<label for="resetPasswordPassword">Password</label>
|
||||
<div class="inputWithButton">
|
||||
<input id="resetPasswordPassword" type="password" name="password" />
|
||||
<button type="button" class="passwordToggle" data-toggles="resetPasswordPassword">
|
||||
Show
|
||||
</button>
|
||||
</div>
|
||||
<span class="inputHint" data-for="resetPasswordPassword">Use at least 8 characters.</span>
|
||||
<output class="validationInfo" for="resetPasswordPassword"></output>
|
||||
<label for="reset-password-email">Email</label>
|
||||
<input id="reset-password-email" type="email" name="email" disabled />
|
||||
<small id="reset-password-email-hint" data-hint-for="reset-password-email"></small>
|
||||
|
||||
<button id="resetPasswordButton">Set password</button>
|
||||
<a id="resetPasswordGoBack" class="linkButton" href="./">Return to log in form</a>
|
||||
<label for="reset-password-password">Password</label>
|
||||
<input id="reset-password-password" type="password" name="password" />
|
||||
<small id="reset-password-password-hint" data-hint-for="reset-password-password"
|
||||
data-hint="Use at least 8 characters."></small>
|
||||
|
||||
<input type="checkbox" role="switch" id="reset-password-password-toggle"
|
||||
class="password-toggle"
|
||||
data-toggles="reset-password-password" />
|
||||
<label for="reset-password-password-toggle">Show password</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<button id="reset-password-submit">Set password</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<a role="button" id="reset-password-go-back" class="outline" href="./">Return to log in form</a>
|
||||
</article>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<div class="row hidden" id="trackingRow">
|
||||
<div class="column">
|
||||
<article id="tracking-part" class="hidden">
|
||||
<header>
|
||||
<h2>Tracked articles</h2>
|
||||
<p id="removeTrackingValidationInfo" class="formValidationInfo">
|
||||
<output class="validationInfo" for="removeTrackingValidationInfo"></output>
|
||||
<button type="button" class="closeButton">×</button>
|
||||
</p>
|
||||
<form id="filterTrackingsForm" novalidate>
|
||||
<label for="filterTrackingsQuery">Filter articles</label>
|
||||
<div class="inputWithButton">
|
||||
<input id="filterTrackingsQuery" type="text" name="query" />
|
||||
<button id="filterTrackingsClear" type="button">Clear</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="trackingsWrapper">
|
||||
<table id="trackings">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<form id="addTrackingForm" novalidate>
|
||||
<p id="addTrackingFormValidationInfo" class="formValidationInfo">
|
||||
<output class="validationInfo" for="addTrackingFormValidationInfo"></output>
|
||||
<button type="button" class="closeButton">×</button>
|
||||
</p>
|
||||
|
||||
<label for="addTrackingPersonName">Track another article</label>
|
||||
<div class="inputWithButton">
|
||||
<!-- TODO: Show random suggestions on who to track -->
|
||||
<input id="addTrackingPersonName" type="text" name="person_name"
|
||||
autocomplete="on" />
|
||||
<button id="addTrackingButton">Add</button>
|
||||
</div>
|
||||
<output class="validationInfo" for="addTrackingPersonName"></output>
|
||||
</form>
|
||||
</header>
|
||||
<article class="status-card hidden" id="remove-trackings-status-card">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
<form id="filter-trackings-form" novalidate>
|
||||
<fieldset>
|
||||
<!--suppress HtmlFormInputWithoutLabel -->
|
||||
<input id="filter-trackings-query" type="search" name="query" />
|
||||
</fieldset>
|
||||
</form>
|
||||
<div id="trackings-wrapper">
|
||||
<table id="trackings">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<form id="add-tracking-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="add-tracking-form">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
|
||||
<div id="settingsRows" class="hidden">
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<fieldset>
|
||||
<!-- TODO: Show random suggestions on who to track -->
|
||||
<label for="add-tracking-name">Track another article</label>
|
||||
<input id="add-tracking-name" type="text" name="person_name" autocomplete="on" />
|
||||
<small id="add-tracking-name-hint" data-hint-for="add-tracking-name"></small>
|
||||
|
||||
<button id="add-tracking-submit">Add</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<div id="settings-part" class="hidden">
|
||||
<article>
|
||||
<header>
|
||||
<h2>Account settings</h2>
|
||||
<form id="logoutForm" novalidate>
|
||||
<p id="logoutFormValidationInfo" class="formValidationInfo">
|
||||
<output class="validationInfo" for="logoutFormValidationInfo"></output>
|
||||
<button type="button" class="closeButton">×</button>
|
||||
</p>
|
||||
</header>
|
||||
<form id="logout-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="logout-form">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
|
||||
<fieldset>
|
||||
<button id="logoutButton">Log out</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<h3>Email</h3>
|
||||
<p>
|
||||
You use your email address to log in.
|
||||
If you have verified your email address, you also receive notifications about tracked
|
||||
articles at this address.
|
||||
You can always disable notifications.
|
||||
</p>
|
||||
<form id="updateEmailForm" novalidate>
|
||||
<p id="updateEmailFormValidationInfo" class="formValidationInfo">
|
||||
<output class="validationInfo" for="updateEmailFormValidationInfo"></output>
|
||||
<button type="button" class="closeButton">×</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<label for="updateEmailEmail">Email address</label>
|
||||
<div class="inputWithButton">
|
||||
<input id="updateEmailEmail" type="email" name="email" autocomplete="on" />
|
||||
<button id="updateEmailButton">Change email</button>
|
||||
</div>
|
||||
<output class="validationInfo" for="updateEmailEmail"></output>
|
||||
</form>
|
||||
<form id="resendEmailVerificationForm" novalidate>
|
||||
<p id="resendEmailVerificationFormValidationInfo" class="formValidationInfo">
|
||||
<output class="validationInfo" for="resendEmailVerificationFormValidationInfo"></output>
|
||||
<button type="button" class="closeButton">×</button>
|
||||
</p>
|
||||
<article>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h3>Email address</h3>
|
||||
<h4>
|
||||
You use your email address to log in.
|
||||
If you have verified your email address, you also receive notifications about tracked
|
||||
articles at this address.
|
||||
You can always disable notifications.
|
||||
</h4>
|
||||
</hgroup>
|
||||
</header>
|
||||
<form id="update-email-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="update-email-form">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
|
||||
<input type="checkbox" id="emailVerifiedCheckbox" disabled />
|
||||
<label for="emailVerifiedCheckbox">Verified</label>
|
||||
<button id="resendEmailVerificationButton" class="hidden">resend link</button>
|
||||
</form>
|
||||
<form id="toggleNotificationsForm" novalidate>
|
||||
<p id="toggleNotificationsFormValidationInfo" class="formValidationInfo">
|
||||
<output class="validationInfo" for="toggleNotificationsFormValidationInfo"></output>
|
||||
<button type="button" class="closeButton">×</button>
|
||||
</p>
|
||||
<fieldset>
|
||||
<label for="update-email-email">Email address</label>
|
||||
<input id="update-email-email" type="email" name="email" autocomplete="on" />
|
||||
<small id="update-email-email-hint" data-hint-for="update-email-email"></small>
|
||||
|
||||
<input type="checkbox" id="notificationsEnabledCheckbox" />
|
||||
<label for="notificationsEnabledCheckbox">Notifications</label>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<button id="updateEmailButton">Change email</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
<form id="resend-email-verification-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="resend-email-verification-form">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
|
||||
<fieldset>
|
||||
<input type="checkbox" id="email-verified-checkbox" disabled />
|
||||
<label for="email-verified-checkbox">Verified</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<button id="resend-email-verification-submit" class="hidden">resend link</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
<form id="toggle-notifications-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="toggle-notifications-form">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
|
||||
<fieldset>
|
||||
<input type="checkbox" id="notifications-enabled-checkbox" />
|
||||
<label for="notifications-enabled-checkbox">Notifications</label>
|
||||
</fieldset>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h3>Password</h3>
|
||||
<form><b>Last changed:</b> <span id="passwordLastChanged">...</span></form>
|
||||
<form id="updatePasswordForm" novalidate>
|
||||
<p id="updatePasswordFormValidationInfo" class="formValidationInfo">
|
||||
<output class="validationInfo" for="updatePasswordFormValidationInfo"></output>
|
||||
<button type="button" class="closeButton">×</button>
|
||||
</p>
|
||||
</header>
|
||||
<form><b>Last changed:</b> <span id="password-last-changed">...</span></form>
|
||||
<form id="update-password-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="update-password-form">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
|
||||
<label for="updatePasswordPasswordOld">Old password</label>
|
||||
<div class="inputWithButton">
|
||||
<input id="updatePasswordPasswordOld" type="password" name="password_old" />
|
||||
<button type="button" class="passwordToggle" data-toggles="updatePasswordPasswordOld">
|
||||
Show
|
||||
</button>
|
||||
</div>
|
||||
<output class="validationInfo" for="updatePasswordPasswordOld"></output>
|
||||
<fieldset>
|
||||
<label for="update-password-password-old">Old password</label>
|
||||
<input id="update-password-password-old" type="password" name="password" />
|
||||
<small id="update-password-password-old-hint"
|
||||
data-hint-for="update-password-password-old"></small>
|
||||
|
||||
<label for="updatePasswordPasswordNew">New password</label>
|
||||
<div class="inputWithButton">
|
||||
<input id="updatePasswordPasswordNew" type="password" name="password_new" />
|
||||
<button type="button" class="passwordToggle" data-toggles="updatePasswordPasswordNew">
|
||||
Show
|
||||
</button>
|
||||
</div>
|
||||
<span class="inputHint"
|
||||
data-for="updatePasswordPasswordNew">Use at least 8 characters.</span>
|
||||
<output class="validationInfo" for="updatePasswordPasswordNew"></output>
|
||||
<input type="checkbox" role="switch" id="update-password-password-old-toggle"
|
||||
class="password-toggle"
|
||||
data-toggles="update-password-password-old" />
|
||||
<label for="update-password-password-old-toggle">Show password</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label for="update-password-password-new">New password</label>
|
||||
<input id="update-password-password-new" type="password" name="password" />
|
||||
<small id="update-password-password-new-hint" data-hint-for="update-password-password-new"
|
||||
data-hint="Use at least 8 characters."></small>
|
||||
|
||||
<input type="checkbox" role="switch" id="update-password-password-new-toggle"
|
||||
class="password-toggle"
|
||||
data-toggles="update-password-password-new" />
|
||||
<label for="update-password-password-new-toggle">Show password</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<button id="updatePasswordButton">Change password</button>
|
||||
</form>
|
||||
<!-- TODO: Add forgot password button after logging in -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<h3>Delete account</h3>
|
||||
<p>
|
||||
If you no longer want to use Death Notifier, you can permanently delete your account.
|
||||
</p>
|
||||
<form id="deleteForm" novalidate>
|
||||
<p id="deleteFormValidationInfo" class="formValidationInfo">
|
||||
<output class="validationInfo" for="deleteFormValidationInfo"></output>
|
||||
<button type="button" class="closeButton">×</button>
|
||||
</p>
|
||||
<input id="deleteEmail" type="hidden" name="email" />
|
||||
<button id="deleteButton">Delete account</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
<!-- TODO: Add forgot password button after logging in -->
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h3>Delete account</h3>
|
||||
<h4>If you no longer want to use Death Notifier, you can permanently delete your
|
||||
account.</h4>
|
||||
</hgroup>
|
||||
</header>
|
||||
<form id="delete-form" novalidate>
|
||||
<article class="status-card hidden" data-status-for="delete-form">
|
||||
<output></output>
|
||||
<a class="close" href="#" aria-label="Close"></a>
|
||||
</article>
|
||||
|
||||
<fieldset>
|
||||
<input id="delete-email" type="hidden" name="email" />
|
||||
<button id="delete-button">Delete account</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<footer id="footer"></footer>
|
||||
</div>
|
||||
<div id="footer"></div>
|
||||
</main>
|
||||
|
||||
<script src="https://static.fwdekker.com/lib/template/2.x.x/template.js?v=%%VERSION_NUMBER%%"></script>
|
||||
<script src="https://static.fwdekker.com/lib/template/3.x.x/template.js?v=%%VERSION_NUMBER%%"></script>
|
||||
<!--suppress HtmlUnknownTarget -->
|
||||
<script src="bundle.js?v=%%VERSION_NUMBER%%"></script>
|
||||
</body>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-ignore
|
||||
const {$} = window.fwdekker;
|
||||
|
||||
import {clearMessages, showError} from "./Message";
|
||||
// @ts-ignore
|
||||
const {clearFormValidity, showInputInvalid, showMessageError} = window.fwdekker.validation;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -11,7 +11,7 @@ export let csrfToken: string | null = null;
|
|||
/**
|
||||
* A shared element to place global messages in.
|
||||
*/
|
||||
export const sharedMessageElement: HTMLFormElement = $("#sharedValidationInfo");
|
||||
export const sharedMessageElement: HTMLFormElement = $("#shared-status-card");
|
||||
|
||||
|
||||
/**
|
||||
|
@ -57,7 +57,7 @@ export function getApi(
|
|||
onSatisfied: (response: ServerResponse) => void = emptyFunction,
|
||||
onUnsatisfied: (response: ServerResponse) => void = emptyFunction,
|
||||
onError: (error: any) => void = emptyFunction,
|
||||
onAlways: (response: ServerResponse|undefined) => void = emptyFunction
|
||||
onAlways: (response: ServerResponse | undefined) => void = emptyFunction
|
||||
): void {
|
||||
interactWithApi(
|
||||
"api.php?" + new URLSearchParams(params), undefined, form,
|
||||
|
@ -81,7 +81,7 @@ export function postApi(
|
|||
onSatisfied: (response: ServerResponse) => void = emptyFunction,
|
||||
onUnsatisfied: (response: ServerResponse) => void = emptyFunction,
|
||||
onError: (error: any) => void = emptyFunction,
|
||||
onAlways: (response: ServerResponse|undefined) => void = emptyFunction
|
||||
onAlways: (response: ServerResponse | undefined) => void = emptyFunction
|
||||
): void {
|
||||
interactWithApi("api.php",
|
||||
{
|
||||
|
@ -115,10 +115,9 @@ function interactWithApi(
|
|||
onSatisfied: (response: ServerResponse) => void = emptyFunction,
|
||||
onUnsatisfied: (response: ServerResponse) => void = emptyFunction,
|
||||
onError: (error: any) => void = emptyFunction,
|
||||
onAlways: (response: ServerResponse|undefined) => void = emptyFunction
|
||||
onAlways: (response: ServerResponse | undefined) => void = emptyFunction
|
||||
): void {
|
||||
clearMessages(form);
|
||||
const topErrorElement = $(`#${form.id}ValidationInfo`) ?? sharedMessageElement;
|
||||
clearFormValidity(form);
|
||||
|
||||
fetch(url, options)
|
||||
.then(it => it.json())
|
||||
|
@ -128,11 +127,14 @@ function interactWithApi(
|
|||
if (!it.satisfied) {
|
||||
if (it.payload.message != null) {
|
||||
if (it.payload.target == null) {
|
||||
showError(topErrorElement, it.payload.message);
|
||||
showMessageError(form, it.payload.message);
|
||||
} else {
|
||||
const target = $(`input[name=${it.payload.target}]`, form);
|
||||
showError(target ?? topErrorElement, it.payload.message);
|
||||
target?.focus();
|
||||
const target = $(`input[name="${it.payload.target}"]`, form);
|
||||
if (target == null) {
|
||||
showMessageError(form, it.payload.message);
|
||||
} else {
|
||||
showInputInvalid(target, it.payload.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,7 +146,7 @@ function interactWithApi(
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
showError(topErrorElement, "Unexpected error. Please try again later.");
|
||||
showMessageError(form, "Unexpected error. Please try again later.");
|
||||
onError(error);
|
||||
onAlways(undefined);
|
||||
});
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
// @ts-ignore
|
||||
const {$, $a, doAfterLoad, footer, header, nav} = window.fwdekker;
|
||||
const {$, $a, doAfterLoad} = window.fwdekker;
|
||||
const {
|
||||
clearFormValidity, clearMessageStatus, showMessageInfo, showMessageError, showMessageSuccess, showMessageWarning
|
||||
// @ts-ignore
|
||||
} = window.fwdekker.validation;
|
||||
|
||||
import {csrfToken, emptyFunction, getApi, postApi, ServerResponse, sharedMessageElement} from "./API";
|
||||
import {CustomEventHandler} from "./CustomEventHandler";
|
||||
import {clearMessage, clearMessages, showError, showInfo, showSuccess, showWarning} from "./Message";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -37,7 +40,7 @@ function refreshTrackings(): void {
|
|||
nameLink.href = "https://en.wikipedia.org/wiki/" + tracking.name;
|
||||
nameLink.innerText = tracking.name;
|
||||
if (tracking.deleted)
|
||||
nameLink.classList.add("redLink");
|
||||
nameLink.classList.add("red-link");
|
||||
nameCell.append(nameLink);
|
||||
row.append(nameCell);
|
||||
|
||||
|
@ -74,8 +77,8 @@ function refreshTrackings(): void {
|
|||
deleteForm,
|
||||
() => {
|
||||
refreshTrackings();
|
||||
showSuccess(
|
||||
$("#removeTrackingValidationInfo"),
|
||||
showMessageSuccess(
|
||||
$("#remove-trackings-status-card"),
|
||||
`Successfully removed <b>${tracking.name}</b>.`
|
||||
);
|
||||
}
|
||||
|
@ -83,6 +86,7 @@ function refreshTrackings(): void {
|
|||
});
|
||||
const deleteButton = document.createElement("button");
|
||||
deleteButton.innerText = "remove";
|
||||
deleteButton.classList.add("outline");
|
||||
deleteForm.append(deleteButton);
|
||||
deleteCell.append(deleteForm);
|
||||
row.append(deleteCell);
|
||||
|
@ -100,8 +104,8 @@ function refreshTrackings(): void {
|
|||
}
|
||||
|
||||
// Scroll to top, re-apply filter
|
||||
$("#trackingsWrapper").scrollTop = 0;
|
||||
$("#filterTrackingsQuery").dispatchEvent(new InputEvent("input"));
|
||||
$("#trackings-wrapper").scrollTop = 0;
|
||||
$("#filter-trackings-query").dispatchEvent(new InputEvent("input"));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -117,23 +121,23 @@ function refreshUserData(): void {
|
|||
const userData = response.payload;
|
||||
|
||||
// Account deletion
|
||||
$("#deleteEmail").value = userData.email;
|
||||
$("#delete-email").value = userData.email;
|
||||
|
||||
// Email
|
||||
$("#updateEmailEmail").value = userData.email;
|
||||
$("#emailVerifiedCheckbox").checked = userData.email_verified;
|
||||
$("#update-email-email").value = userData.email;
|
||||
$("#email-verified-checkbox").checked = userData.email_verified;
|
||||
if (!userData.email_verified)
|
||||
showWarning(
|
||||
showMessageWarning(
|
||||
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);
|
||||
clearMessageStatus(sharedMessageElement);
|
||||
$("#resend-email-verification-submit").classList.toggle("hidden", userData.email_verified);
|
||||
|
||||
// Notifications
|
||||
const notificationsCheckbox = $("#notificationsEnabledCheckbox");
|
||||
const notificationsCheckbox = $("#notifications-enabled-checkbox");
|
||||
notificationsCheckbox.disabled = !userData.email_verified;
|
||||
notificationsCheckbox.checked = userData.email_verified && userData.email_notifications_enabled;
|
||||
|
||||
|
@ -143,7 +147,7 @@ function refreshUserData(): void {
|
|||
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";
|
||||
$("#password-last-changed").innerText = diff === 0 ? "today" : diff + " days ago";
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -154,7 +158,7 @@ function refreshUserData(): void {
|
|||
* @param params the URL's get parameters
|
||||
*/
|
||||
function handleAction(params: URLSearchParams): void {
|
||||
const sharedHomeLink = $("#sharedHomeLink");
|
||||
const sharedHomeLink = $("#shared-home-link");
|
||||
let params_are_valid = true;
|
||||
|
||||
switch (params.get("action")) {
|
||||
|
@ -176,7 +180,7 @@ function handleAction(params: URLSearchParams): void {
|
|||
sharedHomeLink.classList.remove("hidden");
|
||||
redirectWithTimeout(
|
||||
"./", 3, (secondsLeft) => {
|
||||
showSuccess(
|
||||
showMessageSuccess(
|
||||
sharedMessageElement,
|
||||
`Your email address has been verified. ` +
|
||||
`You will be redirected after ${secondsLeft} second(s).`
|
||||
|
@ -201,10 +205,10 @@ function handleAction(params: URLSearchParams): void {
|
|||
},
|
||||
sharedMessageElement,
|
||||
() => {
|
||||
$("#resetPasswordToken").value = params.get("token");
|
||||
$("#resetPasswordEmail").value = params.get("email");
|
||||
$("#resetPasswordRow").classList.remove("hidden");
|
||||
$("#resetPasswordPassword").focus();
|
||||
$("#reset-password-token").value = params.get("token");
|
||||
$("#reset-password-email").value = params.get("email");
|
||||
$("#reset-password-part").classList.remove("hidden");
|
||||
$("#reset-password-password").focus();
|
||||
},
|
||||
() => sharedHomeLink.classList.remove("hidden")
|
||||
);
|
||||
|
@ -218,10 +222,7 @@ function handleAction(params: URLSearchParams): void {
|
|||
|
||||
if (!params_are_valid) {
|
||||
sharedHomeLink.classList.remove("hidden");
|
||||
showError(
|
||||
sharedMessageElement,
|
||||
`Invalid URL.`
|
||||
);
|
||||
showMessageError(sharedMessageElement, `Invalid URL.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,66 +246,41 @@ function redirectWithTimeout(target: string, seconds: number, doEachSecond: (sec
|
|||
}
|
||||
|
||||
|
||||
// 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);
|
||||
clearMessageStatus(sharedMessageElement);
|
||||
|
||||
refreshUserData();
|
||||
refreshTrackings();
|
||||
|
||||
$("#loginRow").classList.add("hidden")
|
||||
$("#trackingRow").classList.remove("hidden");
|
||||
$("#settingsRows").classList.remove("hidden");
|
||||
$("#welcome-part").classList.add("hidden")
|
||||
$("#tracking-part").classList.remove("hidden");
|
||||
$("#settings-part").classList.remove("hidden");
|
||||
});
|
||||
logoutHandler.addListener(() => {
|
||||
clearMessage(sharedMessageElement);
|
||||
clearMessageStatus(sharedMessageElement);
|
||||
|
||||
$("#loginRow").classList.remove("hidden")
|
||||
$("#trackingRow").classList.add("hidden");
|
||||
$("#settingsRows").classList.add("hidden");
|
||||
$("#loginEmail").focus();
|
||||
$("#welcome-part").classList.remove("hidden")
|
||||
$("#tracking-part").classList.add("hidden");
|
||||
$("#settings-part").classList.add("hidden");
|
||||
$("#login-email").focus();
|
||||
});
|
||||
|
||||
// Password visibility toggling
|
||||
$a(".passwordToggle").forEach((toggle: HTMLElement) => {
|
||||
$a(".password-toggle").forEach((toggle: HTMLElement) => {
|
||||
const passwordField = $(`#${toggle.dataset.toggles}`);
|
||||
|
||||
const setState = (showPassword: boolean) => {
|
||||
toggle.innerText = showPassword ? "Hide" : "Show";
|
||||
passwordField.type = showPassword ? "text" : "password";
|
||||
};
|
||||
const setState = (showPassword: boolean) => 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");
|
||||
const loginForm = $("#login-form");
|
||||
loginForm.addEventListener("submit", (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -312,8 +288,8 @@ doAfterLoad(() => {
|
|||
{
|
||||
action: "login",
|
||||
token: csrfToken,
|
||||
email: $("#loginEmail").value,
|
||||
password: $("#loginPassword").value,
|
||||
email: $("#login-email").value,
|
||||
password: $("#login-password").value,
|
||||
},
|
||||
event.target as HTMLFormElement,
|
||||
() => loginHandler.invokeListeners()
|
||||
|
@ -321,10 +297,10 @@ doAfterLoad(() => {
|
|||
});
|
||||
loginHandler.addListener(() => {
|
||||
loginForm.reset();
|
||||
clearMessages(loginForm);
|
||||
clearMessageStatus(loginForm);
|
||||
});
|
||||
|
||||
const registerForm = $("#registerForm");
|
||||
const registerForm = $("#register-form");
|
||||
registerForm.addEventListener("submit", (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -332,27 +308,24 @@ doAfterLoad(() => {
|
|||
{
|
||||
action: "register",
|
||||
token: csrfToken,
|
||||
email: $("#registerEmail").value,
|
||||
password: $("#registerPassword").value
|
||||
email: $("#register-email").value,
|
||||
password: $("#register-password").value
|
||||
},
|
||||
registerForm,
|
||||
() => {
|
||||
// TODO: Add client-side form validation
|
||||
registerForm.reset();
|
||||
showSuccess(
|
||||
$("#registerFormValidationInfo"),
|
||||
"Account created successfully! You may now log in."
|
||||
);
|
||||
$("#loginEmail").focus();
|
||||
showMessageSuccess(registerForm, "Account created successfully! You may now log in.");
|
||||
$("#login-email").focus();
|
||||
}
|
||||
);
|
||||
});
|
||||
loginHandler.addListener(() => {
|
||||
registerForm.reset();
|
||||
clearMessages(registerForm);
|
||||
clearFormValidity(registerForm);
|
||||
});
|
||||
|
||||
const logoutForm = $("#logoutForm");
|
||||
const logoutForm = $("#logout-form");
|
||||
logoutForm.addEventListener("submit", (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -371,7 +344,7 @@ doAfterLoad(() => {
|
|||
|
||||
|
||||
// Forgot password
|
||||
const sendPasswordResetForm = $("#sendPasswordResetForm");
|
||||
const sendPasswordResetForm = $("#send-password-reset-form");
|
||||
sendPasswordResetForm.addEventListener("submit", (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -379,24 +352,21 @@ doAfterLoad(() => {
|
|||
{
|
||||
action: "send-password-reset",
|
||||
token: csrfToken,
|
||||
email: $("#sendPasswordResetEmail").value,
|
||||
email: $("#send-password-reset-email").value,
|
||||
},
|
||||
sendPasswordResetForm,
|
||||
() => {
|
||||
sendPasswordResetForm.reset();
|
||||
showSuccess(
|
||||
$("#sendPasswordResetFormValidationInfo"),
|
||||
"Password reset email sent successfully!"
|
||||
);
|
||||
showMessageSuccess(sendPasswordResetForm, "Password reset email sent successfully!");
|
||||
}
|
||||
);
|
||||
});
|
||||
loginHandler.addListener(() => {
|
||||
sendPasswordResetForm.reset();
|
||||
clearMessages(sendPasswordResetForm);
|
||||
clearFormValidity(sendPasswordResetForm);
|
||||
});
|
||||
|
||||
const resetPasswordForm = $("#resetPasswordForm");
|
||||
const resetPasswordForm = $("#reset-password-form");
|
||||
resetPasswordForm.addEventListener("submit", (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -404,17 +374,17 @@ doAfterLoad(() => {
|
|||
{
|
||||
action: "reset-password",
|
||||
token: csrfToken,
|
||||
email: $("#resetPasswordEmail").value,
|
||||
reset_token: $("#resetPasswordToken").value,
|
||||
password: $("#resetPasswordPassword").value,
|
||||
email: $("#reset-password-email").value,
|
||||
reset_token: $("#reset-password-token").value,
|
||||
password: $("#reset-password-password").value,
|
||||
},
|
||||
resetPasswordForm,
|
||||
() => {
|
||||
$("#resetPasswordForm").reset();
|
||||
resetPasswordForm.reset();
|
||||
redirectWithTimeout(
|
||||
"./", 3, (secondsLeft) => {
|
||||
showSuccess(
|
||||
$("#resetPasswordFormValidationInfo"),
|
||||
showMessageSuccess(
|
||||
resetPasswordForm,
|
||||
`Your password has been updated. You will be redirected after ${secondsLeft} second(s).`
|
||||
);
|
||||
}
|
||||
|
@ -423,35 +393,35 @@ doAfterLoad(() => {
|
|||
);
|
||||
});
|
||||
|
||||
$("#forgotPasswordGoTo").addEventListener("click", (event: MouseEvent) => {
|
||||
$("#forgot-password-go-to").addEventListener("click", (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
$("#loginRow").classList.add("hidden");
|
||||
$("#sendForgotPasswordRow").classList.remove("hidden");
|
||||
$("#welcome-part").classList.add("hidden");
|
||||
$("#send-forgot-password-part").classList.remove("hidden");
|
||||
|
||||
const resetEmail = $("#sendPasswordResetEmail");
|
||||
resetEmail.value = $("#loginEmail").value;
|
||||
const resetEmail = $("#send-password-reset-email");
|
||||
resetEmail.value = $("#login-email").value;
|
||||
resetEmail.focus();
|
||||
});
|
||||
$("#forgotPasswordGoBack").addEventListener("click", (event: MouseEvent) => {
|
||||
$("#forgot-password-go-back").addEventListener("click", (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
$("#sendPasswordResetForm").reset();
|
||||
$("#sendForgotPasswordRow").classList.add("hidden");
|
||||
$("#loginRow").classList.remove("hidden");
|
||||
$("#send-password-reset-form").reset();
|
||||
$("#send-forgot-password-part").classList.add("hidden");
|
||||
$("#welcome-part").classList.remove("hidden");
|
||||
|
||||
const loginEmail = $("#loginEmail");
|
||||
loginEmail.value = $("#sendPasswordResetEmail").value;
|
||||
const loginEmail = $("#login-email");
|
||||
loginEmail.value = $("#send-password-reset-email").value;
|
||||
loginEmail.focus();
|
||||
});
|
||||
|
||||
|
||||
// Account management
|
||||
const deleteForm = $("#deleteForm");
|
||||
const deleteForm = $("#delete-form");
|
||||
deleteForm.addEventListener("submit", (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const actual_email = $("#deleteEmail").value;
|
||||
const actual_email = $("#delete-email").value;
|
||||
const entered_email = window.prompt(
|
||||
`Are you sure you want to delete your account? ` +
|
||||
`This action cannot be undone. ` +
|
||||
|
@ -460,7 +430,7 @@ doAfterLoad(() => {
|
|||
if (entered_email === null) {
|
||||
return;
|
||||
} else if (entered_email !== actual_email) {
|
||||
showError($("#deleteFormValidationInfo"), "Incorrect email address.");
|
||||
showMessageError(deleteForm, "Incorrect email address.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -469,13 +439,13 @@ doAfterLoad(() => {
|
|||
deleteForm,
|
||||
() => {
|
||||
logoutHandler.invokeListeners();
|
||||
showSuccess(sharedMessageElement, "Your account has been deleted.");
|
||||
showMessageSuccess(sharedMessageElement, "Your account has been deleted.");
|
||||
}
|
||||
);
|
||||
});
|
||||
logoutHandler.addListener(() => clearMessages(deleteForm));
|
||||
logoutHandler.addListener(() => clearFormValidity(deleteForm));
|
||||
|
||||
const resendEmailVerificationForm = $("#resendEmailVerificationForm");
|
||||
const resendEmailVerificationForm = $("#resend-email-verification-form");
|
||||
resendEmailVerificationForm.addEventListener("submit", (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -484,17 +454,14 @@ doAfterLoad(() => {
|
|||
resendEmailVerificationForm,
|
||||
() => {
|
||||
refreshUserData();
|
||||
showSuccess(
|
||||
$("#resendEmailVerificationFormValidationInfo"),
|
||||
"Email verification resent successfully!"
|
||||
);
|
||||
showMessageSuccess(resendEmailVerificationForm, "Email verification resent successfully!");
|
||||
}
|
||||
);
|
||||
});
|
||||
logoutHandler.addListener(() => clearMessages(resendEmailVerificationForm));
|
||||
logoutHandler.addListener(() => clearFormValidity(resendEmailVerificationForm));
|
||||
|
||||
const toggleNotificationsForm = $("#toggleNotificationsForm");
|
||||
const notificationsEnabledCheckbox = $("#notificationsEnabledCheckbox");
|
||||
const toggleNotificationsForm = $("#toggle-notifications-form");
|
||||
const notificationsEnabledCheckbox = $("#notifications-enabled-checkbox");
|
||||
notificationsEnabledCheckbox.addEventListener("change", (event: Event) => {
|
||||
event.preventDefault();
|
||||
const enableNotifications = notificationsEnabledCheckbox.checked;
|
||||
|
@ -510,19 +477,19 @@ doAfterLoad(() => {
|
|||
refreshUserData();
|
||||
|
||||
if (enableNotifications)
|
||||
showSuccess($("#toggleNotificationsFormValidationInfo"), "Notifications have been enabled.");
|
||||
showMessageSuccess(toggleNotificationsForm, "Notifications have been enabled.");
|
||||
else
|
||||
showSuccess(
|
||||
$("#toggleNotificationsFormValidationInfo"),
|
||||
showMessageSuccess(
|
||||
toggleNotificationsForm,
|
||||
"Notifications have been disabled. " +
|
||||
"You will still receive security notifications, for example if you change your email address " +
|
||||
"or password.");
|
||||
}
|
||||
);
|
||||
});
|
||||
logoutHandler.addListener(() => clearMessages(toggleNotificationsForm));
|
||||
logoutHandler.addListener(() => clearFormValidity(toggleNotificationsForm));
|
||||
|
||||
const updateEmailForm = $("#updateEmailForm");
|
||||
const updateEmailForm = $("#update-email-form");
|
||||
updateEmailForm.addEventListener("submit", (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -530,14 +497,14 @@ doAfterLoad(() => {
|
|||
{
|
||||
action: "update-email",
|
||||
token: csrfToken,
|
||||
email: $("#updateEmailEmail").value,
|
||||
email: $("#update-email-email").value,
|
||||
},
|
||||
updateEmailForm,
|
||||
() => {
|
||||
updateEmailForm.reset();
|
||||
refreshUserData();
|
||||
showSuccess(
|
||||
$("#updateEmailFormValidationInfo"),
|
||||
showMessageSuccess(
|
||||
updateEmailForm,
|
||||
"Email address updated successfully! " +
|
||||
"Check your inbox for the verification email. " +
|
||||
"You will not receive notifications until you verify your email address."
|
||||
|
@ -547,10 +514,10 @@ doAfterLoad(() => {
|
|||
});
|
||||
logoutHandler.addListener(() => {
|
||||
updateEmailForm.reset();
|
||||
clearMessages(updateEmailForm);
|
||||
clearFormValidity(updateEmailForm);
|
||||
});
|
||||
|
||||
const updatePasswordForm = $("#updatePasswordForm");
|
||||
const updatePasswordForm = $("#update-password-form");
|
||||
updatePasswordForm.addEventListener("submit", (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -558,32 +525,32 @@ doAfterLoad(() => {
|
|||
{
|
||||
action: "update-password",
|
||||
token: csrfToken,
|
||||
password_old: $("#updatePasswordPasswordOld").value,
|
||||
password_new: $("#updatePasswordPasswordNew").value,
|
||||
password_old: $("#update-password-password-old").value,
|
||||
password_new: $("#update-password-password-new").value,
|
||||
},
|
||||
event.target as HTMLFormElement,
|
||||
() => {
|
||||
updatePasswordForm.reset();
|
||||
refreshUserData();
|
||||
showSuccess($("#updatePasswordFormValidationInfo"), "Password updated successfully!");
|
||||
showMessageSuccess(updatePasswordForm, "Password updated successfully!");
|
||||
}
|
||||
);
|
||||
});
|
||||
logoutHandler.addListener(() => {
|
||||
updatePasswordForm.reset();
|
||||
clearMessages(updatePasswordForm);
|
||||
clearFormValidity(updatePasswordForm);
|
||||
});
|
||||
|
||||
|
||||
// Tracking management
|
||||
const queryInput = $("#filterTrackingsQuery");
|
||||
$("#filterTrackingsForm").addEventListener("submit", (event: SubmitEvent) => event.preventDefault());
|
||||
const queryInput = $("#filter-trackings-query");
|
||||
$("#filter-trackings-form").addEventListener("submit", (event: SubmitEvent) => event.preventDefault());
|
||||
queryInput.addEventListener("input", (event: InputEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
$("#trackingsNoMatches")?.remove();
|
||||
$("#trackings-no-matches")?.remove();
|
||||
|
||||
const queryWords = $("#filterTrackingsQuery").value.trim().toLowerCase().split(" ");
|
||||
const queryWords = queryInput.value.trim().toLowerCase().split(" ");
|
||||
let foundMatches = false;
|
||||
$a("#trackings tbody tr").forEach((row: HTMLTableRowElement) => {
|
||||
const rowText = row.innerText.toLowerCase();
|
||||
|
@ -596,7 +563,7 @@ doAfterLoad(() => {
|
|||
|
||||
if (!foundMatches) {
|
||||
const row = document.createElement("tr");
|
||||
row.id = "trackingsNoMatches";
|
||||
row.id = "trackings-no-matches";
|
||||
row.classList.add("placeholder");
|
||||
const cell = document.createElement("td");
|
||||
cell.colSpan = 3;
|
||||
|
@ -605,12 +572,8 @@ doAfterLoad(() => {
|
|||
$("#trackings tbody").appendChild(row);
|
||||
}
|
||||
});
|
||||
$("#filterTrackingsClear").addEventListener("click", () => {
|
||||
queryInput.value = "";
|
||||
queryInput.dispatchEvent(new InputEvent("input"));
|
||||
});
|
||||
|
||||
const addTrackingForm = $("#addTrackingForm");
|
||||
const addTrackingForm = $("#add-tracking-form");
|
||||
addTrackingForm.addEventListener("submit", (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -618,15 +581,15 @@ doAfterLoad(() => {
|
|||
{
|
||||
action: "add-tracking",
|
||||
token: csrfToken,
|
||||
person_name: $("#addTrackingPersonName").value,
|
||||
person_name: $("#add-tracking-name").value,
|
||||
},
|
||||
addTrackingForm,
|
||||
(response: ServerResponse) => {
|
||||
addTrackingForm.reset();
|
||||
refreshTrackings();
|
||||
|
||||
showSuccess(
|
||||
$("#addTrackingFormValidationInfo"),
|
||||
showMessageSuccess(
|
||||
addTrackingForm,
|
||||
response.payload["renamed"]
|
||||
? (
|
||||
`Successfully added <b>${response.payload["input"]}</b> as ` +
|
||||
|
@ -639,7 +602,7 @@ doAfterLoad(() => {
|
|||
});
|
||||
logoutHandler.addListener(() => {
|
||||
addTrackingForm.reset();
|
||||
clearMessages(addTrackingForm);
|
||||
clearFormValidity(addTrackingForm);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -661,9 +624,11 @@ doAfterLoad(() => {
|
|||
emptyFunction,
|
||||
(response) => {
|
||||
// Always execute the following
|
||||
$("main").classList.remove("hidden");
|
||||
|
||||
const message = (response?.payload ?? {})["global_message"];
|
||||
if (message != null)
|
||||
showInfo($("#globalMessage"), message);
|
||||
showMessageInfo($("#global-message"), message);
|
||||
|
||||
handleAction(params);
|
||||
}
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
// @ts-ignore
|
||||
const {$, $a} = window.fwdekker;
|
||||
|
||||
|
||||
/**
|
||||
* Shows a message of the given type.
|
||||
*
|
||||
* @param type the type of message to show, or `null` if the message should be hidden
|
||||
* @param element the element to show the message at. This is typically an `input` element or an element with CSS class
|
||||
* `formValidationInfo`. If a `label` exists with `for` attribute referring to `input`, the `label` is also
|
||||
* appropriately styled
|
||||
* @param message the message to display, or `undefined` if the message should be hidden. The message is shown in the
|
||||
* `span` with attribute `data-for` containing the ID of `element`
|
||||
*/
|
||||
function showMessage(type: "info" | "success" | "warning" | "error" | null, element: HTMLElement,
|
||||
message?: string): void {
|
||||
element.classList.remove("hasMessage", "info", "success", "warning", "error");
|
||||
if (type != null && message != null) element.classList.add("hasMessage", type);
|
||||
if (element.id == null) return;
|
||||
|
||||
const label = $(`label[for="${element.id}"]`);
|
||||
if (label != null) {
|
||||
label.classList.remove("hasMessage", "info", "success", "warning", "error");
|
||||
if (type != null && message != null) label.classList.add("hasMessage", type);
|
||||
}
|
||||
|
||||
const info = $(`output[for="${element.id}"]`);
|
||||
if (info != null) {
|
||||
info.classList.remove("hasMessage", "info", "success", "warning", "error");
|
||||
if (type != null && message != null) info.classList.add("hasMessage", type);
|
||||
info.innerHTML = message ?? "";
|
||||
}
|
||||
|
||||
const hint = $(`span[data-for="${element.id}"]`);
|
||||
if (hint != null)
|
||||
hint.classList.toggle("hidden", type != null && message != null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clears all validation messages from the given form.
|
||||
*
|
||||
* @param form the form to clear all validation messages in
|
||||
*/
|
||||
export function clearMessages(form: HTMLFormElement): void {
|
||||
const formValidationInfo = $(`#${form.id}ValidationInfo`);
|
||||
if (formValidationInfo != null) clearMessage(formValidationInfo);
|
||||
|
||||
$a("input", form).forEach(clearMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the validation message from the given element.
|
||||
*
|
||||
* @param element the element to clear the validation message from
|
||||
*/
|
||||
export function clearMessage(element: HTMLElement): void {
|
||||
showMessage(null, element, undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows an info message at the given element.
|
||||
*
|
||||
* @param element the element to show the info message at
|
||||
* @param message the info message to display
|
||||
*/
|
||||
export function showInfo(element: HTMLElement, message?: string): void {
|
||||
showMessage("info", element, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a success message at the given element.
|
||||
*
|
||||
* @param element the element to show the success message at
|
||||
* @param message the success message to display
|
||||
*/
|
||||
export function showSuccess(element: HTMLElement, message?: string): void {
|
||||
showMessage("success", element, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a warning message at the given element.
|
||||
*
|
||||
* @param element the element to show the warning message at
|
||||
* @param message the warning message to display
|
||||
*/
|
||||
export function showWarning(element: HTMLElement, message?: string): void {
|
||||
showMessage("warning", element, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an error message at the given element.
|
||||
*
|
||||
* @param element the element to show the error message at
|
||||
* @param message the error message to display
|
||||
*/
|
||||
export function showError(element: HTMLElement, message?: string): void {
|
||||
showMessage("error", element, message);
|
||||
}
|
Loading…
Reference in New Issue