nordabiz/templates/auth/register.html
2026-01-01 14:01:49 +01:00

627 lines
18 KiB
HTML

{% extends "base.html" %}
{% block title %}Rejestracja - Norda Biznes Hub{% 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);
}
.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);
}
.form-help {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--spacing-xs);
}
.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: "✓";
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%;
}
.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;
}
.nip-status {
padding: var(--spacing-md);
border-radius: var(--radius);
font-size: var(--font-size-sm);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.nip-status.norda-member {
background-color: #d1fae5;
border: 1px solid #10b981;
color: #065f46;
}
.nip-status.non-member {
background-color: #dbeafe;
border: 1px solid #3b82f6;
color: #1e40af;
}
.nip-status.error {
background-color: #fee2e2;
border: 1px solid #ef4444;
color: #991b1b;
}
.nip-status .icon {
font-size: var(--font-size-lg);
font-weight: bold;
}
.nip-status.loading {
background-color: #f3f4f6;
border: 1px solid #d1d5db;
color: #4b5563;
}
.btn-secondary {
background-color: var(--surface);
color: var(--text-primary);
border: 2px solid var(--border);
padding: var(--spacing-sm) var(--spacing-lg);
border-radius: var(--radius);
font-weight: 500;
transition: var(--transition);
cursor: pointer;
}
.btn-secondary:hover {
background-color: var(--background);
border-color: var(--primary);
}
.btn-secondary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.email-status {
font-size: var(--font-size-sm);
padding: var(--spacing-xs) 0;
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.email-status.available {
color: var(--success);
}
.email-status.taken {
color: var(--error);
}
.email-status.checking {
color: var(--text-secondary);
}
</style>
{% endblock %}
{% block content %}
<div class="auth-container">
<div class="auth-card">
<div class="auth-header">
<h1>Utwórz konto</h1>
<p>Dołącz do społeczności Norda Biznes</p>
</div>
<form method="POST" action="{{ url_for('register') }}" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="name" class="form-label">
Imię i nazwisko <span class="required">*</span>
</label>
<input
type="text"
id="name"
name="name"
class="form-input"
placeholder="Jan Kowalski"
required
autocomplete="name"
autofocus
>
</div>
<div class="form-group">
<label for="email" class="form-label">
Adres email <span class="required">*</span>
</label>
<input
type="email"
id="email"
name="email"
class="form-input"
placeholder="twoj@email.com"
required
autocomplete="email"
>
<div id="emailStatus" class="form-help" style="display: none; margin-top: var(--spacing-xs);"></div>
</div>
<div class="form-group">
<label for="company_nip" class="form-label">
NIP firmy <span class="required">*</span>
</label>
<div style="display: flex; gap: var(--spacing-sm);">
<input
type="text"
id="company_nip"
name="company_nip"
class="form-input"
placeholder="0000000000"
maxlength="10"
required
style="flex: 1;"
>
<button
type="button"
id="verifyNipBtn"
class="btn btn-secondary"
style="white-space: nowrap;"
>
Sprawdź NIP
</button>
</div>
<div class="form-help">
Podaj 10 cyfr bez spacji i myślników (np. 5882465814)
</div>
<div id="nipStatus" class="nip-status" style="display: none; margin-top: var(--spacing-sm);"></div>
</div>
<div class="form-group">
<label for="password" class="form-label">
Hasło <span class="required">*</span>
</label>
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="••••••••"
required
autocomplete="new-password"
>
<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 znaków</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">Mała 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-actions">
<button type="submit" class="btn btn-primary btn-lg btn-full" id="submitBtn" disabled>
Zarejestruj się
</button>
</div>
</form>
<div class="auth-footer">
<p>Masz już konto? <a href="{{ url_for('login') }}">Zaloguj się</a></p>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
// Version: 2025-11-24 14:00 - Live checkbox validation
console.log('🔧 Password validation loaded - Version 2025-11-24 14:00');
const passwordInput = document.getElementById('password');
const strengthBar = document.getElementById('strengthBar');
const submitBtn = document.getElementById('submitBtn');
// Password strength checker
passwordInput.addEventListener('input', function() {
const password = this.value;
let strength = 0;
let validCount = 0;
// Check requirements
const hasLength = password.length >= 8;
const hasUpper = /[A-Z]/.test(password);
const hasLower = /[a-z]/.test(password);
const hasDigit = /\d/.test(password);
// Update UI for each requirement
updateRequirement('req-length', hasLength);
updateRequirement('req-upper', hasUpper);
updateRequirement('req-lower', hasLower);
updateRequirement('req-digit', hasDigit);
// Calculate strength
if (hasLength) { strength++; validCount++; }
if (hasUpper) { strength++; validCount++; }
if (hasLower) { strength++; validCount++; }
if (hasDigit) { strength++; validCount++; }
// Update strength bar
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');
}
// Enable submit button only if all requirements met
submitBtn.disabled = validCount < 4;
});
function updateRequirement(id, valid) {
const el = document.getElementById(id);
console.log(`Updating ${id}: ${valid ? 'VALID' : 'invalid'}`); // DEBUG
if (valid) {
el.classList.add('valid');
} else {
el.classList.remove('valid');
}
}
// Form validation
document.querySelector('form').addEventListener('submit', function(e) {
const name = document.getElementById('name');
const email = document.getElementById('email');
const password = document.getElementById('password');
let valid = true;
// Name validation
if (!name.value || name.value.length < 2) {
name.classList.add('error');
valid = false;
} else {
name.classList.remove('error');
}
// Email validation
if (!email.value || !email.value.includes('@')) {
email.classList.add('error');
valid = false;
} else {
email.classList.remove('error');
}
// Password validation
const hasLength = password.value.length >= 8;
const hasUpper = /[A-Z]/.test(password.value);
const hasLower = /[a-z]/.test(password.value);
const hasDigit = /\d/.test(password.value);
if (!hasLength || !hasUpper || !hasLower || !hasDigit) {
password.classList.add('error');
valid = false;
} else {
password.classList.remove('error');
}
if (!valid) {
e.preventDefault();
}
});
// Email validation and availability check
const emailInput = document.getElementById('email');
const emailStatus = document.getElementById('emailStatus');
let emailCheckTimeout;
let emailAvailable = false;
emailInput.addEventListener('input', function() {
const email = this.value.trim();
// Clear previous timeout
clearTimeout(emailCheckTimeout);
// Basic format validation
if (!email) {
emailStatus.style.display = 'none';
emailAvailable = false;
return;
}
// Proper email format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
if (!emailRegex.test(email)) {
showEmailStatus('taken', '❌ Nieprawidłowy format email');
emailAvailable = false;
return;
}
// Check availability after 500ms of no typing
emailCheckTimeout = setTimeout(() => {
checkEmailAvailability(email);
}, 500);
});
function checkEmailAvailability(email) {
showEmailStatus('checking', '⏳ Sprawdzam dostępność...');
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
fetch('/api/check-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ email: email })
})
.then(response => response.json())
.then(data => {
if (data.available) {
showEmailStatus('available', '✅ Email dostępny');
emailAvailable = true;
} else {
showEmailStatus('taken', '❌ Email jest już zarejestrowany');
emailAvailable = false;
}
})
.catch(error => {
console.error('Email check error:', error);
emailStatus.style.display = 'none';
emailAvailable = false;
});
}
function showEmailStatus(statusClass, message) {
emailStatus.className = 'email-status ' + statusClass;
emailStatus.textContent = message;
emailStatus.style.display = 'block';
}
// NIP verification
const verifyNipBtn = document.getElementById('verifyNipBtn');
const nipInput = document.getElementById('company_nip');
const nipStatus = document.getElementById('nipStatus');
let nipVerified = false;
let isNordaMember = false;
verifyNipBtn.addEventListener('click', function() {
const nip = nipInput.value.trim();
// Validate NIP format (10 digits)
if (!/^\d{10}$/.test(nip)) {
showNipStatus('error', '❌ Nieprawidłowy format NIP. Podaj 10 cyfr.');
return;
}
// Show loading state
showNipStatus('loading', '⏳ Sprawdzam NIP...');
verifyNipBtn.disabled = true;
// Get CSRF token from form
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
// Call API
fetch('/api/verify-nip', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ nip: nip })
})
.then(response => response.json())
.then(data => {
nipVerified = true;
isNordaMember = data.is_member;
if (data.is_member) {
showNipStatus('norda-member',
`✅ ${data.company_name}<br><strong>Firma należy do sieci NORDA</strong> - Konto uprzywilejowane`
);
} else {
showNipStatus('non-member',
`✅ NIP zweryfikowany<br>Firma spoza sieci NORDA - Konto standardowe`
);
}
})
.catch(error => {
console.error('NIP verification error:', error);
showNipStatus('error', '❌ Błąd weryfikacji NIP. Spróbuj ponownie.');
nipVerified = false;
})
.finally(() => {
verifyNipBtn.disabled = false;
});
});
function showNipStatus(statusClass, message) {
nipStatus.className = 'nip-status ' + statusClass;
nipStatus.innerHTML = '<span class="icon"></span>' + message;
nipStatus.style.display = 'flex';
}
// Clear status when NIP is modified
nipInput.addEventListener('input', function() {
if (nipVerified) {
nipStatus.style.display = 'none';
nipVerified = false;
isNordaMember = false;
}
});
{% endblock %}