Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions evidence-sharing/checkout/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,9 @@ form.addEventListener("submit", (event) => {
status.className = "status ok";
status.textContent = `${code} applied: ${discount.percent}% off`;
});

const welcome = new URLSearchParams(location.search).get("welcome");
if (welcome) {
const name = welcome.charAt(0).toUpperCase() + welcome.slice(1);
document.querySelector(".step").textContent = `Checkout · Signed in as ${name}`;
}
71 changes: 71 additions & 0 deletions evidence-sharing/checkout/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sign in — Northwind Supply</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body class="login-page">
<header class="topbar">
<span class="brand">Northwind Supply</span>
<span class="step">Sign in</span>
</header>

<main class="login">
<section class="login-card" aria-labelledby="login-heading">
<h1 id="login-heading">Sign in to Northwind</h1>
<p id="form-status" class="form-status" role="alert"></p>

<form id="login-form" novalidate>
<div class="field">
<label for="email">Email</label>
<input
id="email"
name="email"
type="email"
autocomplete="username"
aria-describedby="email-error"
/>
<p id="email-error" class="field-error"></p>
</div>

<div class="field">
<label for="password">Password</label>
<div class="password-row">
<input
id="password"
name="password"
type="password"
autocomplete="current-password"
aria-describedby="password-error"
/>
<button
id="toggle-password"
class="toggle"
type="button"
aria-controls="password"
aria-pressed="false"
>
Show
</button>
</div>
<p id="password-error" class="field-error"></p>
</div>

<div class="options">
<label class="remember">
<input id="remember" name="remember" type="checkbox" />
Remember me
</label>
<a class="forgot" href="#">Forgot password?</a>
</div>

<button id="sign-in" class="sign-in" type="submit">Sign in</button>
</form>
</section>
</main>

<script src="login.js"></script>
</body>
</html>
86 changes: 86 additions & 0 deletions evidence-sharing/checkout/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const DEMO_EMAIL = "ana@northwind.test";
const DEMO_PASSWORD = "correct-horse";
const SIGN_IN_DELAY_MS = 600;
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

const form = document.getElementById("login-form");
const email = document.getElementById("email");
const password = document.getElementById("password");
const emailError = document.getElementById("email-error");
const passwordError = document.getElementById("password-error");
const formStatus = document.getElementById("form-status");
const toggle = document.getElementById("toggle-password");
const signIn = document.getElementById("sign-in");

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function emailMessage() {
const value = email.value.trim();
if (!value) return "Enter your email";
if (!EMAIL_PATTERN.test(value)) return "Enter a valid email address";
return "";
}

function passwordMessage() {
return password.value.length < 8
? "Password must be at least 8 characters"
: "";
}

function showFieldError(input, errorEl, message) {
errorEl.textContent = message;
input.setAttribute("aria-invalid", String(Boolean(message)));
}

function validate() {
const emailText = emailMessage();
const passwordText = passwordMessage();
showFieldError(email, emailError, emailText);
showFieldError(password, passwordError, passwordText);
return !emailText && !passwordText;
}

function setSigningIn(busy) {
signIn.disabled = busy;
signIn.textContent = busy ? "Signing in…" : "Sign in";
}

form.addEventListener("submit", async (event) => {
event.preventDefault();
formStatus.textContent = "";

if (!validate()) {
(emailMessage() ? email : password).focus();
return;
}

setSigningIn(true);
await wait(SIGN_IN_DELAY_MS);

const matches =
email.value.trim().toLowerCase() === DEMO_EMAIL &&
password.value === DEMO_PASSWORD;
if (matches) {
location.href = "index.html?welcome=ana";
return;
}

setSigningIn(false);
formStatus.textContent = "Email or password is incorrect";
password.focus();
});

email.addEventListener("input", () => {
if (email.getAttribute("aria-invalid") === "true") validate();
});

password.addEventListener("input", () => {
if (password.getAttribute("aria-invalid") === "true") validate();
});

toggle.addEventListener("click", () => {
const showing = password.type === "text";
password.type = showing ? "password" : "text";
toggle.textContent = showing ? "Show" : "Hide";
toggle.setAttribute("aria-pressed", String(!showing));
});
154 changes: 154 additions & 0 deletions evidence-sharing/checkout/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,157 @@ h2 {
width: 100%;
padding: 12px;
}

.login-page {
display: flex;
flex-direction: column;
min-height: 100vh;
background: var(--panel);
}

.login {
flex: 1;
display: grid;
place-items: center;
padding: 48px 16px;
}

.login-card {
box-sizing: border-box;
width: 100%;
max-width: 400px;
background: #fff;
border: 1px solid var(--line);
border-radius: 8px;
padding: 32px;
}

.login-card h1 {
font-size: 24px;
margin-bottom: 20px;
}

.form-status {
min-height: 0;
margin: 0 0 16px;
font-size: 15px;
color: var(--bad);
}

.form-status:empty {
display: none;
}

.login-card .field + .field {
margin-top: 16px;
}

.login-card label {
display: block;
font-size: 15px;
font-weight: 600;
margin-bottom: 6px;
}

.login-card input[type="email"],
.login-card input[type="password"] {
box-sizing: border-box;
width: 100%;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 6px;
font: inherit;
font-size: 16px;
color: var(--ink);
}

.login-card input[aria-invalid="true"] {
border-color: var(--bad);
}

.password-row {
display: flex;
gap: 8px;
}

.password-row input {
flex: 1;
min-width: 0;
}

.toggle {
border: 1px solid var(--line);
border-radius: 6px;
background: #fff;
color: var(--ink);
font: inherit;
font-size: 15px;
padding: 0 14px;
cursor: pointer;
}

.field-error {
margin: 6px 0 0;
font-size: 15px;
color: var(--bad);
}

.field-error:empty {
display: none;
}

.options {
display: flex;
justify-content: space-between;
align-items: center;
margin: 20px 0 24px;
font-size: 15px;
}

.login-card .remember {
display: flex;
align-items: center;
gap: 8px;
font-weight: 400;
margin: 0;
cursor: pointer;
}

.login-card .remember input {
width: 16px;
height: 16px;
margin: 0;
accent-color: var(--accent);
}

.forgot {
color: var(--accent);
text-decoration: none;
}

.forgot:hover {
text-decoration: underline;
}

.sign-in {
width: 100%;
border: 0;
border-radius: 6px;
background: var(--accent);
color: #fff;
font: inherit;
font-size: 16px;
font-weight: 600;
padding: 12px;
cursor: pointer;
}

.sign-in:disabled {
opacity: 0.6;
cursor: progress;
}

.login-page :focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}