Skip to content

Commit 34c547f

Browse files
committed
Add password verification feature with UI and validation rules
1 parent 053d1ae commit 34c547f

File tree

6 files changed

+343
-2
lines changed

6 files changed

+343
-2
lines changed

css/styles.css

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,4 +535,33 @@ footer {
535535
100% {
536536
transform: translate3d(0,-100%,0);
537537
}
538-
}
538+
}
539+
540+
/* =============================
541+
Anchor offset for sticky navigation
542+
Ensures in-page anchors (e.g., #about) are scrolled below the sticky nav
543+
by providing a top padding to the scroll container and per-element margins.
544+
Adjust --nav-height if the nav height changes.
545+
============================= */
546+
:root {
547+
/* Approximate sticky nav height (padding + line-height). Tweak as needed. */
548+
--nav-height: 40px;
549+
}
550+
551+
/* If the nav wraps into two lines on smaller screens, increase the offset */
552+
@media (max-width: 900px) {
553+
:root { --nav-height: 120px; }
554+
}
555+
556+
/* Native offset for hash navigation and programmatic scrolling */
557+
html {
558+
scroll-padding-top: var(--nav-height);
559+
scroll-behavior: smooth; /* Optional: comment out if not desired */
560+
}
561+
562+
/* Ensure any element with an id (typical anchor targets) sits below nav */
563+
[id] {
564+
scroll-margin-top: var(--nav-height);
565+
}
566+
567+

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ <h4 id="rewireddate">2005-03 — PRESENT</h4>
9090
</p>
9191
<h2 id="projects">PROJECTS</h2>
9292
<div class="projects">
93-
<a href="projects/index.html" title="View Projects" aria-label="View Projects">
93+
<a href="projects/index.html" title="View Projects" aria-label="View Projects">
9494
<img src="assets/coding.webp" alt="Coding project" title="Coding projects">
9595
</a>
9696
<img src="assets/design.webp" alt="Design project" title="Design projects">

projects/index.html

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="stylesheet" href="../css/styles.css">
6+
<link rel="preconnect" href="https://fonts.googleapis.com">
7+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
8+
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
9+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
10+
<link rel="icon" href="../assets/favicon.ico" type="image/x-icon"/>
11+
<title>Projects • Alex Jessup</title>
12+
</head>
13+
<body>
14+
<nav>
15+
<a href="../index.html" class="nav">HOME</a>
16+
<a href="../index.html#about" class="nav">ABOUT</a>
17+
<a href="../index.html#experience" class="nav">EXPERIENCE</a>
18+
<a href="../index.html#projects" class="nav">PROJECTS</a>
19+
<a href="../index.html#skills" class="nav">SKILLS</a>
20+
<a href="../index.html#contact" class="nav">CONTACT</a>
21+
</nav>
22+
23+
<h1 id="banner">PROJECTS</h1>
24+
25+
<section class="projects">
26+
<!-- Example project cards. Add more as needed. -->
27+
<a class="project-card" href="password-verification/index.html" title="Password Verification Demo">
28+
<img src="../assets/coding.webp" alt="Coding projects" />
29+
<div class="project-title">Password Verification</div>
30+
</a>
31+
</section>
32+
33+
<footer>
34+
Copyright © 2025 <a href="https://www.linkedin.com/in/alexpjessup/" target="_blank">Alex Jessup</a>
35+
</footer>
36+
</body>
37+
</html>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/* Center text/placeholder visually across the whole input group (input + toggle button)
2+
by compensating the space taken by the right-side button. */
3+
4+
.form-control.center-compensated {
5+
text-align: center; /* center the input text and placeholder */
6+
/* Add LEFT padding equal to the toggle button width so the visual center
7+
aligns with the center of the whole input group (input + button). */
8+
padding-left: var(--toggle-offset, 2.75rem);
9+
}
10+
11+
/* Ensure placeholder inherits centering in some browsers */
12+
.form-control.center-compensated::placeholder {
13+
text-align: center;
14+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Password Checker</title>
7+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
8+
<link rel="stylesheet" href="../../css/styles.css">
9+
<link rel="stylesheet" href="css/styles.css">
10+
</head>
11+
<body>
12+
<nav>
13+
<a href="../../index.html" class="nav">HOME</a>
14+
<a href="../../index.html#about" class="nav">ABOUT</a>
15+
<a href="../../index.html#experience" class="nav">EXPERIENCE</a>
16+
<a href="../../index.html#projects" class="nav">PROJECTS</a>
17+
<a href="../../index.html#skills" class="nav">SKILLS</a>
18+
<a href="../../index.html#contact" class="nav">CONTACT</a>
19+
</nav>
20+
<script src="scripts/script.js"></script>
21+
<h2 class="mt-3 text-center">Password Complexity Verification</h2>
22+
<div class="container mt-4 align-content-center w-50 border p-4 rounded-3">
23+
<div id="alertContainer" class="mt-2"></div>
24+
<ul class="list-group list-group-flush">
25+
<li id="rule-length" class="list-group-item">At least 8 characters long</li>
26+
<li id="rule-upper" class="list-group-item">Contains at least one uppercase letter</li>
27+
<li id="rule-lower" class="list-group-item">Contains at least one lowercase letter</li>
28+
<li id="rule-number" class="list-group-item">Contains at least one number</li>
29+
<li id="rule-special" class="list-group-item">Contains a special character (e.g., !, @, #, $, %, ^, &, *)</li>
30+
<li id="rule-match" class="list-group-item">Both password entries must match</li>
31+
</ul>
32+
<div class="input-group mt-4">
33+
<input
34+
type="password"
35+
id="passwordInput"
36+
class="form-control text-center center-compensated"
37+
placeholder="Enter your password"
38+
oninput="inputPassword(this.value)"
39+
aria-describedby="togglePassword"
40+
>
41+
<button
42+
class="btn btn-outline-secondary"
43+
type="button"
44+
id="togglePassword"
45+
title="Show / hide password"
46+
onclick="(function(btn){const pw=document.getElementById('passwordInput'); if(pw.type==='password'){pw.type='text'; btn.textContent='🙈'; } else {pw.type='password'; btn.textContent='👁️';} adjustCentering();})(this)"
47+
aria-pressed="false"
48+
aria-label="Toggle password visibility"
49+
>👁️</button>
50+
</div>
51+
52+
<div class="input-group mt-2">
53+
<input
54+
type="password"
55+
id="passwordVerify"
56+
class="form-control text-center center-compensated"
57+
placeholder="Re-enter your password"
58+
oninput="inputPassword(this.value)"
59+
aria-describedby="togglePassword"
60+
>
61+
<button
62+
class="btn btn-outline-secondary"
63+
type="button"
64+
id="togglePasswordVerify"
65+
title="Show / hide password"
66+
onclick="(function(btn){const pw=document.getElementById('passwordVerify'); if(pw.type==='password'){pw.type='text'; btn.textContent='🙈'; } else {pw.type='password'; btn.textContent='👁️';} adjustCentering();})(this)"
67+
aria-pressed="false"
68+
aria-label="Toggle password visibility"
69+
>👁️</button>
70+
</div>
71+
72+
<button id="savePasswordBtn" class="btn btn-secondary mt-3 d-block mx-auto" onclick="savePassword()">Copy Password</button>
73+
</div>
74+
</body>
75+
</html>
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
Password rules:
3+
- Minimum length = 8
4+
- Must contain at least:
5+
- 1 number
6+
- 1 lowercase letter
7+
- 1 uppercase letter
8+
- 1 special character
9+
- Passwords must match
10+
*/
11+
12+
// Helper: toggle success/danger styles for a rule item
13+
function setRuleState(id, isMet) {
14+
const el = document.getElementById(id);
15+
if (!el) return;
16+
el.classList.toggle('list-group-item-success', !!isMet);
17+
el.classList.toggle('list-group-item-danger', !isMet);
18+
}
19+
20+
// Evaluate password and update UI
21+
function updateRules() {
22+
const pw = document.getElementById('passwordInput')?.value || '';
23+
const pw2 = document.getElementById('passwordVerify')?.value || '';
24+
25+
const hasLen = pw.length >= 8;
26+
const hasUpper = /[A-Z]/.test(pw);
27+
const hasLower = /[a-z]/.test(pw);
28+
const hasDigit = /\d/.test(pw);
29+
const hasSpecial = /[!@#$%^&*()\-_=+\[\]{};:\"'<>.,?\/\\|`~]/.test(pw);
30+
const matches = pw.length > 0 && pw === pw2;
31+
32+
setRuleState('rule-length', hasLen);
33+
setRuleState('rule-upper', hasUpper);
34+
setRuleState('rule-lower', hasLower);
35+
setRuleState('rule-number', hasDigit);
36+
setRuleState('rule-special', hasSpecial);
37+
setRuleState('rule-match', matches);
38+
39+
return { hasLen, hasUpper, hasLower, hasDigit, hasSpecial, matches };
40+
}
41+
42+
// Called by inline oninput handlers
43+
function inputPassword() {
44+
updateRules();
45+
}
46+
47+
// Called by the "Check Password" button
48+
function validatePassword() {
49+
const { hasLen, hasUpper, hasLower, hasDigit, hasSpecial, matches } = updateRules();
50+
const ok = hasLen && hasUpper && hasLower && hasDigit && hasSpecial && matches;
51+
if (!ok) {
52+
alert('Password does not meet all requirements yet.');
53+
} else {
54+
alert('Great! Your password meets all requirements.');
55+
}
56+
return ok;
57+
}
58+
59+
// Save Password: copy current password to clipboard and alert
60+
function copyTextToClipboard(text) {
61+
if (navigator.clipboard && window.isSecureContext) {
62+
return navigator.clipboard.writeText(text);
63+
}
64+
// Fallback for insecure contexts/older browsers
65+
return new Promise((resolve, reject) => {
66+
try {
67+
const ta = document.createElement('textarea');
68+
ta.value = text;
69+
ta.setAttribute('readonly', '');
70+
ta.style.position = 'fixed';
71+
ta.style.left = '-9999px';
72+
document.body.appendChild(ta);
73+
ta.select();
74+
const successful = document.execCommand('copy');
75+
document.body.removeChild(ta);
76+
successful ? resolve() : reject(new Error('Copy command was unsuccessful'));
77+
} catch (err) {
78+
reject(err);
79+
}
80+
});
81+
}
82+
83+
// Show a Bootstrap-style alert in the page (fallback to alert())
84+
function showAlert(message, type = 'success', timeout = 4000) {
85+
const container = document.getElementById('alertContainer');
86+
if (!container) {
87+
alert(message);
88+
return;
89+
}
90+
// Replace any existing alert
91+
container.innerHTML = '';
92+
93+
const wrapper = document.createElement('div');
94+
wrapper.className = `alert alert-${type} alert-dismissible fade show`;
95+
wrapper.setAttribute('role', 'alert');
96+
wrapper.textContent = message;
97+
98+
const btn = document.createElement('button');
99+
btn.type = 'button';
100+
btn.className = 'btn-close';
101+
btn.setAttribute('aria-label', 'Close');
102+
btn.addEventListener('click', () => wrapper.remove());
103+
104+
wrapper.appendChild(btn);
105+
container.appendChild(wrapper);
106+
107+
if (timeout && timeout > 0) {
108+
setTimeout(() => {
109+
try { wrapper.classList.remove('show'); } catch (e) {}
110+
// Remove after transition (~150ms), keep it simple
111+
setTimeout(() => wrapper.remove(), 200);
112+
}, timeout);
113+
}
114+
}
115+
116+
function savePassword() {
117+
// Evaluate and sync UI
118+
const { hasLen, hasUpper, hasLower, hasDigit, hasSpecial, matches } = updateRules();
119+
const pw = document.getElementById('passwordInput')?.value || '';
120+
const pw2 = document.getElementById('passwordVerify')?.value || '';
121+
122+
if (!pw) {
123+
showAlert('Please enter a password.', 'warning');
124+
return false;
125+
}
126+
127+
// Ensure both fields are filled and match
128+
if (!pw2 || !matches) {
129+
showAlert('Passwords do not match and cannot be copied. Please make both entries identical.', 'warning');
130+
return false;
131+
}
132+
133+
// Ensure all complexity requirements are met
134+
if (!(hasLen && hasUpper && hasLower && hasDigit && hasSpecial)) {
135+
showAlert('Password does not meet all requirements and cannot be copied yet.', 'warning');
136+
return false;
137+
}
138+
139+
// Copy to clipboard
140+
copyTextToClipboard(pw)
141+
.then(() => {
142+
showAlert('Password has been saved to the clipboard.', 'success');
143+
})
144+
.catch((err) => {
145+
console.error('Clipboard error:', err);
146+
showAlert('Sorry, could not copy the password to the clipboard.', 'danger');
147+
});
148+
return true;
149+
}
150+
151+
// Measure the toggle button widths and adjust input padding so text/placeholder
152+
// appear centered relative to the entire input group.
153+
function adjustCentering() {
154+
try {
155+
const pairs = [
156+
{ inputId: 'passwordInput', btnId: 'togglePassword' },
157+
{ inputId: 'passwordVerify', btnId: 'togglePasswordVerify' },
158+
];
159+
pairs.forEach(({ inputId, btnId }) => {
160+
const input = document.getElementById(inputId);
161+
const btn = document.getElementById(btnId);
162+
if (!input || !btn) return;
163+
const btnRect = btn.getBoundingClientRect();
164+
// Use measured width; minimal buffer to account for borders
165+
const offset = Math.ceil(btnRect.width) + 1;
166+
input.style.setProperty('--toggle-offset', offset + 'px');
167+
});
168+
} catch (_) {
169+
// no-op
170+
}
171+
}
172+
173+
// Initialize styles once DOM is ready (covers refresh / prefilled fields)
174+
function initUI() {
175+
updateRules();
176+
adjustCentering();
177+
}
178+
179+
if (document.readyState === 'loading') {
180+
document.addEventListener('DOMContentLoaded', initUI);
181+
} else {
182+
initUI();
183+
}
184+
185+
// Keep centering accurate on resize
186+
window.addEventListener('resize', adjustCentering);

0 commit comments

Comments
 (0)