nordabiz/templates/auth/reset_password.html
Maciej Pienczyn ca2da75d3b
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
fix: prominent password match indicator and disabled button styling
Large red/green indicator below confirm field shows match status.
Submit button is visually gray when disabled, turns green with shadow
when all conditions met.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 10:28:06 +01:00

535 lines
17 KiB
HTML
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Nowe haslo - Norda Biznes Partner{% endblock %}
{% block container_class %}container-narrow{% endblock %}
{% block extra_css %}
<style>
.auth-container {
max-width: 480px;
margin: 0 auto;
padding: var(--spacing-2xl) 0;
}
.auth-card {
background-color: var(--surface);
padding: var(--spacing-2xl);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-lg);
}
.auth-header {
text-align: center;
margin-bottom: var(--spacing-xl);
}
.auth-header h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.auth-header p {
color: var(--text-secondary);
}
.auth-icon {
width: 64px;
height: 64px;
background: linear-gradient(135deg, var(--success), #059669);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto var(--spacing-lg);
}
.auth-icon svg {
width: 32px;
height: 32px;
color: white;
}
.form-group {
margin-bottom: var(--spacing-lg);
}
.form-label {
display: block;
font-weight: 500;
margin-bottom: var(--spacing-sm);
color: var(--text-primary);
}
.form-label .required {
color: var(--error);
}
.form-input {
width: 100%;
padding: var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: var(--font-size-base);
font-family: var(--font-family);
transition: var(--transition);
}
.form-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.form-input.error {
border-color: var(--error);
}
.password-strength {
margin-top: var(--spacing-sm);
height: 4px;
background-color: var(--border);
border-radius: var(--radius-sm);
overflow: hidden;
}
.password-strength-bar {
height: 100%;
width: 0;
transition: var(--transition);
}
.password-strength-bar.weak {
background-color: var(--error);
width: 33%;
}
.password-strength-bar.medium {
background-color: var(--warning);
width: 66%;
}
.password-strength-bar.strong {
background-color: var(--success);
width: 100%;
}
.password-requirements {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--spacing-sm);
}
.password-requirements ul {
list-style: none;
padding: 0;
margin: var(--spacing-sm) 0 0 0;
}
.password-requirements li {
padding: var(--spacing-xs) 0;
display: flex;
align-items: center;
gap: var(--spacing-sm);
transition: all 0.2s ease;
}
.password-requirements li .checkbox-icon {
width: 20px;
height: 20px;
border: 2px solid var(--border);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
background: var(--surface);
transition: all 0.2s ease;
flex-shrink: 0;
}
.password-requirements li.valid .checkbox-icon {
background: var(--success);
border-color: var(--success);
}
.password-requirements li.valid .checkbox-icon::after {
content: "\2713";
color: white;
font-weight: bold;
font-size: 14px;
}
.password-requirements li.valid {
color: var(--success);
font-weight: 500;
}
.form-actions {
margin-top: var(--spacing-xl);
}
.btn-full {
width: 100%;
}
/* Match indicator under confirm field */
.match-indicator {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
padding: 10px 14px;
border-radius: var(--radius);
font-size: 15px;
font-weight: 600;
transition: all 0.3s ease;
}
.match-indicator.mismatch {
background: #fef2f2;
border: 2px solid #ef4444;
color: #dc2626;
}
.match-indicator.match {
background: #f0fdf4;
border: 2px solid #22c55e;
color: #16a34a;
}
.match-icon {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
color: white;
flex-shrink: 0;
}
.match-indicator.mismatch .match-icon {
background: #ef4444;
}
.match-indicator.match .match-icon {
background: #22c55e;
}
/* Submit button states */
#submitBtn {
background-color: #d1d5db;
color: #9ca3af;
cursor: not-allowed;
transition: all 0.3s ease;
}
#submitBtn:not(:disabled) {
background-color: var(--success);
color: white;
cursor: pointer;
box-shadow: 0 4px 12px rgba(22, 163, 74, 0.4);
}
#submitBtn:not(:disabled):hover {
background-color: #059669;
box-shadow: 0 6px 16px rgba(22, 163, 74, 0.5);
}
.auth-footer {
text-align: center;
margin-top: var(--spacing-lg);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border);
color: var(--text-secondary);
}
.auth-footer a {
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.auth-footer a:hover {
text-decoration: underline;
}
/* Password toggle */
.password-wrapper {
position: relative;
}
.password-wrapper .form-input {
padding-right: 48px;
}
.password-toggle {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: center;
}
.password-toggle:hover {
color: var(--text-primary);
}
.password-toggle svg {
width: 20px;
height: 20px;
}
</style>
{% endblock %}
{% block content %}
<div class="auth-container">
<div class="auth-card">
<div class="auth-header">
<div class="auth-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</div>
<h1>Ustaw nowe haslo</h1>
<p>Wprowadz nowe haslo do swojego konta</p>
</div>
<form method="POST" action="{{ url_for('reset_password', token=token) }}" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="password" class="form-label">
Nowe haslo <span class="required">*</span>
</label>
<div class="password-wrapper">
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="********"
required
autocomplete="new-password"
autofocus
>
<button type="button" class="password-toggle" onclick="togglePassword('password', this)" title="Pokaż/ukryj hasło">
<svg class="eye-open" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<svg class="eye-closed" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="display:none;">
<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24"/>
<line x1="1" y1="1" x2="23" y2="23"/>
</svg>
</button>
</div>
<div class="password-strength">
<div class="password-strength-bar" id="strengthBar"></div>
</div>
<div class="password-requirements">
<ul id="requirements">
<li id="req-length">
<span class="checkbox-icon"></span>
<span class="requirement-text">Minimum 8 znakow</span>
</li>
<li id="req-upper">
<span class="checkbox-icon"></span>
<span class="requirement-text">Wielka litera</span>
</li>
<li id="req-lower">
<span class="checkbox-icon"></span>
<span class="requirement-text">Mala litera</span>
</li>
<li id="req-digit">
<span class="checkbox-icon"></span>
<span class="requirement-text">Cyfra</span>
</li>
</ul>
</div>
</div>
<div class="form-group">
<label for="password_confirm" class="form-label">
Potwierdz haslo <span class="required">*</span>
</label>
<div class="password-wrapper">
<input
type="password"
id="password_confirm"
name="password_confirm"
class="form-input"
placeholder="********"
required
autocomplete="new-password"
>
<button type="button" class="password-toggle" onclick="togglePassword('password_confirm', this)" title="Pokaż/ukryj hasło">
<svg class="eye-open" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<svg class="eye-closed" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="display:none;">
<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24"/>
<line x1="1" y1="1" x2="23" y2="23"/>
</svg>
</button>
</div>
<div id="matchIndicator" class="match-indicator" style="display:none;">
<span class="match-icon" id="matchIcon"></span>
<span class="match-text" id="matchText"></span>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-lg btn-full" id="submitBtn" disabled>
Zmien haslo
</button>
</div>
</form>
<div class="auth-footer">
<p><a href="{{ url_for('login') }}">Powrot do logowania</a></p>
</div>
</div>
</div>
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1100; display: flex; flex-direction: column; gap: 10px;"></div>
<style>
.toast { padding: 12px 20px; border-radius: var(--radius); background: var(--surface); border-left: 4px solid var(--primary); box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 10px; animation: toastIn 0.3s ease; }
.toast.error { border-left-color: var(--error); }
@keyframes toastIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
</style>
{% endblock %}
{% block extra_js %}
// Password toggle function
function togglePassword(inputId, button) {
const input = document.getElementById(inputId);
const eyeOpen = button.querySelector('.eye-open');
const eyeClosed = button.querySelector('.eye-closed');
if (input.type === 'password') {
input.type = 'text';
eyeOpen.style.display = 'none';
eyeClosed.style.display = 'block';
} else {
input.type = 'password';
eyeOpen.style.display = 'block';
eyeClosed.style.display = 'none';
}
}
function showToast(message, type = 'info', duration = 4000) {
const container = document.getElementById('toastContainer');
const icons = { error: '✕', info: '' };
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `<span style="font-size:1.2em">${icons[type]||''}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration);
}
const passwordInput = document.getElementById('password');
const passwordConfirm = document.getElementById('password_confirm');
const strengthBar = document.getElementById('strengthBar');
const submitBtn = document.getElementById('submitBtn');
passwordInput.addEventListener('input', function() {
const password = this.value;
let strength = 0;
let validCount = 0;
const hasLength = password.length >= 8;
const hasUpper = /[A-Z]/.test(password);
const hasLower = /[a-z]/.test(password);
const hasDigit = /\d/.test(password);
updateRequirement('req-length', hasLength);
updateRequirement('req-upper', hasUpper);
updateRequirement('req-lower', hasLower);
updateRequirement('req-digit', hasDigit);
if (hasLength) { strength++; validCount++; }
if (hasUpper) { strength++; validCount++; }
if (hasLower) { strength++; validCount++; }
if (hasDigit) { strength++; validCount++; }
strengthBar.className = 'password-strength-bar';
if (strength === 1 || strength === 2) {
strengthBar.classList.add('weak');
} else if (strength === 3) {
strengthBar.classList.add('medium');
} else if (strength === 4) {
strengthBar.classList.add('strong');
}
checkFormValidity();
});
passwordConfirm.addEventListener('input', checkFormValidity);
function updateRequirement(id, valid) {
const el = document.getElementById(id);
if (valid) {
el.classList.add('valid');
} else {
el.classList.remove('valid');
}
}
const matchIndicator = document.getElementById('matchIndicator');
const matchIcon = document.getElementById('matchIcon');
const matchText = document.getElementById('matchText');
function checkFormValidity() {
const password = passwordInput.value;
const confirm = passwordConfirm.value;
const hasLength = password.length >= 8;
const hasUpper = /[A-Z]/.test(password);
const hasLower = /[a-z]/.test(password);
const hasDigit = /\d/.test(password);
const passwordsMatch = password === confirm && confirm.length > 0;
// Show match indicator only when user started typing confirmation
if (confirm.length > 0) {
matchIndicator.style.display = 'flex';
if (passwordsMatch) {
matchIndicator.className = 'match-indicator match';
matchIcon.textContent = '\u2713';
matchText.textContent = 'Hasła są identyczne';
} else {
matchIndicator.className = 'match-indicator mismatch';
matchIcon.textContent = '\u2717';
matchText.textContent = 'Hasła nie są identyczne';
}
} else {
matchIndicator.style.display = 'none';
}
submitBtn.disabled = !(hasLength && hasUpper && hasLower && hasDigit && passwordsMatch);
}
document.querySelector('form').addEventListener('submit', function(e) {
const password = passwordInput.value;
const confirm = passwordConfirm.value;
if (password !== confirm) {
passwordConfirm.classList.add('error');
e.preventDefault();
showToast('Hasła nie są identyczne', 'error');
}
});
{% endblock %}