nordabiz/templates/auth/register.html
Maciej Pienczyn 6d589407be Sync local repo with production state
- Add MembershipFee and MembershipFeeConfig models
- Add /health endpoint for monitoring
- Add Microsoft Fluent Design CSS
- Update templates with new CSS structure
- Add Announcement model
- Update .gitignore to exclude analysis files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

843 lines
25 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);
}
/* Company autocomplete styles */
.company-search-container {
position: relative;
}
.company-search-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);
}
.company-search-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.company-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 250px;
overflow-y: auto;
background: var(--surface);
border: 1px solid var(--border);
border-top: none;
border-radius: 0 0 var(--radius) var(--radius);
box-shadow: var(--shadow-lg);
z-index: 1000;
display: none;
}
.company-dropdown.show {
display: block;
}
.company-option {
padding: var(--spacing-md);
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border);
transition: background-color 0.15s ease;
}
.company-option:last-child {
border-bottom: none;
}
.company-option:hover {
background-color: var(--primary-light, #eff6ff);
}
.company-option-name {
font-weight: 500;
color: var(--text-primary);
}
.company-option-city {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.company-selected {
padding: var(--spacing-md);
background-color: #d1fae5;
border: 1px solid #10b981;
border-radius: var(--radius);
margin-top: var(--spacing-sm);
display: flex;
justify-content: space-between;
align-items: center;
}
.company-selected-info {
flex: 1;
}
.company-selected-name {
font-weight: 600;
color: #065f46;
}
.company-selected-nip {
font-size: var(--font-size-sm);
color: #047857;
}
.company-selected-badge {
background: #10b981;
color: white;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 600;
}
.company-clear-btn {
background: none;
border: none;
color: #065f46;
cursor: pointer;
padding: var(--spacing-xs);
margin-left: var(--spacing-sm);
font-size: var(--font-size-lg);
}
.company-clear-btn:hover {
color: #991b1b;
}
.no-results {
padding: var(--spacing-md);
text-align: center;
color: var(--text-secondary);
font-style: italic;
}
</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_search" class="form-label">
Firma członkowska Norda Biznes <span class="required">*</span>
</label>
<div class="company-search-container">
<input
type="text"
id="company_search"
class="company-search-input"
placeholder="Zacznij wpisywać nazwę firmy..."
autocomplete="off"
>
<div id="companyDropdown" class="company-dropdown"></div>
</div>
<div class="form-help">
Wpisz nazwę firmy członkowskiej - lista filtruje się automatycznie
</div>
<!-- Selected company display -->
<div id="companySelected" class="company-selected" style="display: none;">
<div class="company-selected-info">
<div class="company-selected-name" id="selectedCompanyName"></div>
<div class="company-selected-nip">NIP: <span id="selectedCompanyNip"></span></div>
</div>
<span class="company-selected-badge">NORDA BIZNES</span>
<button type="button" class="company-clear-btn" id="clearCompanyBtn" title="Zmień firmę"></button>
</div>
<!-- Hidden NIP field for form submission -->
<input type="hidden" id="company_nip" name="company_nip" required>
</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';
}
// ============================================
// Company Autocomplete with Real-time Filtering
// ============================================
const companySearch = document.getElementById('company_search');
const companyDropdown = document.getElementById('companyDropdown');
const companySelected = document.getElementById('companySelected');
const selectedCompanyName = document.getElementById('selectedCompanyName');
const selectedCompanyNip = document.getElementById('selectedCompanyNip');
const clearCompanyBtn = document.getElementById('clearCompanyBtn');
const nipHiddenInput = document.getElementById('company_nip');
let companies = []; // Will be loaded from API
let selectedCompany = null;
// Load companies on page load
loadCompanies();
async function loadCompanies() {
try {
const response = await fetch('/api/companies');
const data = await response.json();
companies = data.companies || data;
console.log(`Loaded ${companies.length} companies for autocomplete`);
} catch (error) {
console.error('Failed to load companies:', error);
companies = [];
}
}
// Filter and show dropdown on input
companySearch.addEventListener('input', function() {
const query = this.value.trim().toLowerCase();
if (query.length === 0) {
hideDropdown();
return;
}
// Filter companies - match anywhere in name
const filtered = companies.filter(company =>
company.name.toLowerCase().includes(query)
).slice(0, 10); // Limit to 10 results
showDropdown(filtered, query);
});
// Show dropdown on focus if there's text
companySearch.addEventListener('focus', function() {
if (this.value.trim().length > 0 && !selectedCompany) {
const query = this.value.trim().toLowerCase();
const filtered = companies.filter(c => c.name.toLowerCase().includes(query)).slice(0, 10);
showDropdown(filtered, query);
}
});
function showDropdown(filteredCompanies, query) {
if (filteredCompanies.length === 0) {
companyDropdown.innerHTML = '<div class="no-results">Nie znaleziono firmy o nazwie "' + escapeHtml(query) + '"</div>';
companyDropdown.classList.add('show');
return;
}
companyDropdown.innerHTML = filteredCompanies.map(company => `
<div class="company-option" data-nip="${company.nip || ''}" data-name="${escapeHtml(company.name)}">
<span class="company-option-name">${highlightMatch(company.name, query)}</span>
<span class="company-option-city">${company.city || ''}</span>
</div>
`).join('');
// Add click handlers
companyDropdown.querySelectorAll('.company-option').forEach(option => {
option.addEventListener('click', function() {
selectCompany(this.dataset.name, this.dataset.nip);
});
});
companyDropdown.classList.add('show');
}
function hideDropdown() {
companyDropdown.classList.remove('show');
}
function selectCompany(name, nip) {
selectedCompany = { name, nip };
// Update hidden NIP field
nipHiddenInput.value = nip;
// Show selected company card
selectedCompanyName.textContent = name;
selectedCompanyNip.textContent = nip;
companySelected.style.display = 'flex';
// Hide search input and dropdown
companySearch.style.display = 'none';
hideDropdown();
console.log(`Selected company: ${name} (NIP: ${nip})`);
}
// Clear selection
clearCompanyBtn.addEventListener('click', function() {
selectedCompany = null;
nipHiddenInput.value = '';
companySelected.style.display = 'none';
companySearch.style.display = 'block';
companySearch.value = '';
companySearch.focus();
});
// Hide dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!companySearch.contains(e.target) && !companyDropdown.contains(e.target)) {
hideDropdown();
}
});
// Keyboard navigation
companySearch.addEventListener('keydown', function(e) {
const options = companyDropdown.querySelectorAll('.company-option');
const active = companyDropdown.querySelector('.company-option:hover, .company-option.active');
if (e.key === 'ArrowDown') {
e.preventDefault();
if (options.length > 0) {
const next = active ? active.nextElementSibling || options[0] : options[0];
options.forEach(o => o.classList.remove('active'));
next.classList.add('active');
next.scrollIntoView({ block: 'nearest' });
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (options.length > 0) {
const prev = active ? active.previousElementSibling || options[options.length - 1] : options[options.length - 1];
options.forEach(o => o.classList.remove('active'));
prev.classList.add('active');
prev.scrollIntoView({ block: 'nearest' });
}
} else if (e.key === 'Enter') {
e.preventDefault();
const activeOption = companyDropdown.querySelector('.company-option.active');
if (activeOption) {
selectCompany(activeOption.dataset.name, activeOption.dataset.nip);
}
} else if (e.key === 'Escape') {
hideDropdown();
}
});
// Helper functions
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function highlightMatch(text, query) {
if (!query) return escapeHtml(text);
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return escapeHtml(text).replace(regex, '<strong>$1</strong>');
}
{% endblock %}