improve: unified v3 email design across all templates
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

Shared _email_v3_wrap() helper: branded header with logo, full footer
with address/links. Updated: password reset, welcome, forum reply,
role notification. Action buttons grid layout in /admin/users.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-21 18:14:45 +01:00
parent 40ee5db139
commit 9b0a0bea56
2 changed files with 136 additions and 169 deletions

View File

@ -339,20 +339,50 @@ init_email_service()
# ============================================================ # ============================================================
# EMAIL TEMPLATES # EMAIL TEMPLATES (v3 design)
# ============================================================ # ============================================================
def _email_v3_wrap(title: str, subtitle: str, content_html: str) -> str:
"""Wrap email content in v3 branded shell (header + footer)."""
return f'''<!DOCTYPE html>
<html lang="pl">
<head><meta charset="UTF-8"></head>
<body style="margin:0; padding:0; background:#f1f5f9; font-family: 'Inter', Arial, sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f1f5f9; padding: 32px 0;">
<tr><td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background:#ffffff; border-radius:12px; overflow:hidden; box-shadow: 0 4px 24px rgba(0,0,0,0.08);">
<tr><td style="background: linear-gradient(135deg, #1e40af, #172554); padding: 36px 32px; text-align: center;">
<img src="https://nordabiznes.pl/static/images/logo-circle.png" width="64" height="64" alt="NB" style="border-radius:50%; margin-bottom:16px; border: 2px solid rgba(255,255,255,0.3);">
<h1 style="margin:0; color:#ffffff; font-size:24px; font-weight:700;">{title}</h1>
<p style="margin:8px 0 0; color:#93c5fd; font-size:14px;">{subtitle}</p>
</td></tr>
<tr><td style="padding: 32px;">
{content_html}
</td></tr>
<tr><td style="background:#f8fafc; padding: 24px; text-align:center; border-top: 1px solid #e2e8f0;">
<p style="margin:0 0 4px; color:#1e293b; font-size:14px; font-weight:600;">Norda Biznes Partner</p>
<p style="margin:0 0 2px; color:#94a3b8; font-size:12px;">Stowarzyszenie Norda Biznes</p>
<p style="margin:0 0 12px; color:#94a3b8; font-size:12px;">ul. 12 Marca 238/5, 84-200 Wejherowo</p>
<p style="margin:0 0 12px; color:#94a3b8; font-size:13px;">
<a href="https://nordabiznes.pl" style="color:#2563eb; text-decoration:none;">nordabiznes.pl</a>
&nbsp;|&nbsp;
<a href="https://www.facebook.com/nordabiznes" style="color:#2563eb; text-decoration:none;">Facebook</a>
</p>
<p style="margin:0; color:#cbd5e1; font-size:11px;">To powiadomienie zostało wysłane automatycznie.</p>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>'''
def send_password_reset_email(email: str, reset_url: str) -> bool: def send_password_reset_email(email: str, reset_url: str) -> bool:
""" """Send password reset email."""
Send password reset email
Args:
email: Recipient email address
reset_url: Password reset URL with token
Returns:
True if sent successfully, False otherwise
"""
subject = "Reset hasła - Norda Biznes Partner" subject = "Reset hasła - Norda Biznes Partner"
body_text = f"""Otrzymałeś ten email, ponieważ zażądano resetowania hasła dla Twojego konta Norda Biznes Partner. body_text = f"""Otrzymałeś ten email, ponieważ zażądano resetowania hasła dla Twojego konta Norda Biznes Partner.
@ -365,61 +395,42 @@ Link będzie ważny przez 1 godzinę.
Jeśli nie zażądałeś resetowania hasła, zignoruj ten email. Jeśli nie zażądałeś resetowania hasła, zignoruj ten email.
--- ---
Norda Biznes Partner - Platforma Networkingu Regionalnej Izby Przedsiębiorców Norda Biznes Partner
https://nordabiznes.pl https://nordabiznes.pl
""" """
body_html = f""" content = f'''
<!DOCTYPE html> <p style="margin:0 0 20px; color:#1e293b; font-size:16px;">Otrzymałeś ten email, ponieważ zażądano resetowania hasła dla Twojego konta.</p>
<html>
<head>
<meta charset="UTF-8">
<style>
body {{ font-family: 'Inter', Arial, sans-serif; line-height: 1.6; color: #1e293b; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; background: #f8fafc; }}
.header {{ background-color: #1e3a8a; background: linear-gradient(135deg, #1e40af, #1e3a8a); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }}
.header h1 {{ margin: 0; font-size: 28px; font-weight: 700; text-shadow: 0 2px 4px rgba(0,0,0,0.3); }}
.content {{ background: white; padding: 30px; border-radius: 0 0 8px 8px; }}
.button {{ display: inline-block; padding: 14px 32px; background: #2563eb; color: white; text-decoration: none; border-radius: 8px; margin: 20px 0; font-weight: 600; }}
.button:hover {{ background: #1e40af; }}
.footer {{ text-align: center; padding: 20px; color: #64748b; font-size: 0.9em; }}
.warning {{ background: #fef3c7; border-left: 4px solid #f59e0b; padding: 15px; margin: 20px 0; border-radius: 0 8px 8px 0; color: #92400e; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Reset hasła</h1>
</div>
<div class="content">
<p>Otrzymałeś ten email, ponieważ zażądano resetowania hasła dla Twojego konta Norda Biznes Partner.</p>
<p>Aby zresetować hasło, kliknij w poniższy przycisk:</p> <p style="margin:0 0 24px; color:#475569; font-size:15px;">Kliknij poniższy przycisk, aby ustawić nowe hasło:</p>
<center> <table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:24px;">
<a href="{reset_url}" class="button">Zresetuj hasło</a> <tr><td align="center" style="padding: 8px 0;">
</center> <a href="{reset_url}" style="display:inline-block; padding:16px 40px; background: linear-gradient(135deg, #dc2626, #991b1b); color:#ffffff; text-decoration:none; border-radius:8px; font-size:15px; font-weight:600;">Zresetuj hasło</a>
</td></tr>
</table>
<div class="warning"> <!-- Warning -->
<strong>Ważność linku:</strong> 1 godzina <table width="100%" cellpadding="0" cellspacing="0" style="background:#fef3c7; border-radius:8px; border: 1px solid #fcd34d; margin-bottom:24px;">
</div> <tr><td style="padding: 16px;">
<p style="margin:0 0 4px; color:#92400e; font-size:14px; font-weight:600;">Ważność linku: 1 godzina</p>
<p style="margin:0; color:#92400e; font-size:13px;">Po tym czasie konieczne będzie ponowne żądanie resetu hasła.</p>
</td></tr>
</table>
<p>Jeśli przycisk nie działa, skopiuj i wklej poniższy link do przeglądarki:</p> <p style="margin:0 0 8px; color:#64748b; font-size:13px;">Jeśli przycisk nie działa, skopiuj i wklej ten link:</p>
<p style="word-break: break-all; color: #2563eb;">{reset_url}</p> <p style="margin:0 0 24px; color:#2563eb; font-size:13px; word-break:break-all;">{reset_url}</p>
<p style="margin-top: 30px; color: #64748b; font-size: 0.9em;"> <table width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc; border-radius:8px; border: 1px solid #e2e8f0;">
<strong>Nie zażądałeś resetowania hasła?</strong><br> <tr><td style="padding: 16px;">
Zignoruj ten email. Twoje hasło pozostanie bez zmian. <p style="margin:0; color:#64748b; font-size:14px;">
</p> <strong>Nie zażądałeś resetowania hasła?</strong><br>
</div> Zignoruj ten email Twoje hasło pozostanie bez zmian.
<div class="footer"> </p>
<p><strong>Norda Biznes Partner</strong> - Platforma Networkingu Regionalnej Izby Przedsiębiorców</p> </td></tr>
<p><a href="https://nordabiznes.pl">nordabiznes.pl</a></p> </table>'''
</div>
</div> body_html = _email_v3_wrap('Reset hasła', 'Norda Biznes Partner', content)
</body>
</html>
"""
return send_email( return send_email(
to=[email], to=[email],
@ -431,95 +442,59 @@ https://nordabiznes.pl
def send_welcome_email(email: str, name: str, verification_url: str) -> bool: def send_welcome_email(email: str, name: str, verification_url: str) -> bool:
""" """Send welcome/verification email after registration."""
Send welcome/verification email after registration subject = "Witamy w Norda Biznes Partner — Potwierdź email"
Args:
email: Recipient email address
name: User's name
verification_url: Email verification URL with token
Returns:
True if sent successfully, False otherwise
"""
subject = "Witamy w Norda Biznes Partner - Potwierdź email"
body_text = f"""Witaj {name}! body_text = f"""Witaj {name}!
Dziękujemy za rejestrację w Norda Biznes Partner - platformie networkingu Regionalnej Izby Przedsiębiorców Norda Biznes. Dziękujemy za rejestrację w Norda Biznes Partner.
Aby aktywować swoje konto, potwierdź adres email klikając w poniższy link: Aby aktywować konto, kliknij: {verification_url}
{verification_url} Link ważny 24 godziny.
Link będzie ważny przez 24 godziny.
Po potwierdzeniu email będziesz mógł:
- Przeglądać profile firm członkowskich Izby
- Korzystać z asystenta AI do wyszukiwania usług
- Nawiązywać kontakty biznesowe
Pozdrawiamy, Pozdrawiamy,
Zespół Norda Biznes Partner Zespół Norda Biznes Partner
---
Norda Biznes Partner - Platforma Networkingu Regionalnej Izby Przedsiębiorców
https://nordabiznes.pl https://nordabiznes.pl
""" """
body_html = f""" check = (
<!DOCTYPE html> '<div style="width:28px; height:28px; background:#16a34a; border-radius:50%; '
<html> 'text-align:center; line-height:28px; display:inline-block; margin-right:10px;">'
<head> '<span style="color:#fff; font-size:16px; font-weight:bold;">&#10003;</span></div>'
<meta charset="UTF-8"> )
<style>
body {{ font-family: 'Inter', Arial, sans-serif; line-height: 1.6; color: #1e293b; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; background: #f8fafc; }}
.header {{ background-color: #1e3a8a; background: linear-gradient(135deg, #1e40af, #1e3a8a); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }}
.header h1 {{ margin: 0; font-size: 28px; font-weight: 700; text-shadow: 0 2px 4px rgba(0,0,0,0.3); }}
.content {{ background: white; padding: 30px; border-radius: 0 0 8px 8px; }}
.button {{ display: inline-block; padding: 14px 32px; background: #10b981; color: white; text-decoration: none; border-radius: 8px; margin: 20px 0; font-weight: 600; }}
.footer {{ text-align: center; padding: 20px; color: #64748b; font-size: 0.9em; }}
.features {{ background: #f0fdf4; padding: 20px; border-radius: 8px; margin: 20px 0; color: #166534; }}
.features ul {{ margin: 10px 0; padding-left: 20px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Witamy w Norda Biznes Partner!</h1>
</div>
<div class="content">
<p>Witaj <strong>{name}</strong>!</p>
<p>Dziękujemy za rejestrację w Norda Biznes Partner - platformie networkingu Regionalnej Izby Przedsiębiorców Norda Biznes.</p> content = f'''
<p style="margin:0 0 16px; color:#1e293b; font-size:16px;">Witaj <strong>{name}</strong>!</p>
<p style="margin:0 0 28px; color:#475569; font-size:15px; line-height:1.5;">Dziękujemy za rejestrację w Norda Biznes Partner platformie networkingu Regionalnej Izby Przedsiębiorców.</p>
<p>Aby aktywować swoje konto, potwierdź adres email:</p> <p style="margin:0 0 16px; color:#1e293b; font-size:15px;">Aby aktywować konto, potwierdź swój adres email:</p>
<center> <table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:28px;">
<a href="{verification_url}" class="button">Potwierdź email</a> <tr><td align="center" style="padding: 8px 0;">
</center> <a href="{verification_url}" style="display:inline-block; padding:16px 40px; background: linear-gradient(135deg, #16a34a, #15803d); color:#ffffff; text-decoration:none; border-radius:8px; font-size:15px; font-weight:600;">Potwierdź email</a>
</td></tr>
</table>
<div class="features"> <p style="margin:0 0 14px; color:#1e293b; font-size:16px; font-weight:600;">Co zyskujesz:</p>
<strong>Po potwierdzeniu email będziesz mógł:</strong> <table width="100%" cellpadding="0" cellspacing="0" style="background:#f8fdf8; border-radius:10px; border: 1px solid #d1fae5; margin-bottom:28px;">
<ul> <tr><td style="padding: 14px 16px; border-bottom: 1px solid #e2e8f0;">
<li>Przeglądać profile firm członkowskich Izby</li> {check}<span style="color:#1e293b; font-size:15px; font-weight:500;">Katalog firm członkowskich Izby</span>
<li>Korzystać z asystenta AI do wyszukiwania usług</li> </td></tr>
<li>Nawiązywać kontakty biznesowe</li> <tr><td style="padding: 14px 16px; border-bottom: 1px solid #e2e8f0;">
</ul> {check}<span style="color:#1e293b; font-size:15px; font-weight:500;">Asystent AI do wyszukiwania usług</span>
</div> </td></tr>
<tr><td style="padding: 14px 16px;">
{check}<span style="color:#1e293b; font-size:15px; font-weight:500;">Networking i kontakty biznesowe</span>
</td></tr>
</table>
<p style="color: #64748b; font-size: 0.9em;"> <table width="100%" cellpadding="0" cellspacing="0" style="background:#fef3c7; border-radius:8px; border: 1px solid #fcd34d;">
Link będzie ważny przez 24 godziny. <tr><td style="padding: 14px 16px;">
</p> <p style="margin:0; color:#92400e; font-size:13px;">Link aktywacyjny jest ważny przez <strong>24 godziny</strong>.</p>
</div> </td></tr>
<div class="footer"> </table>'''
<p><strong>Norda Biznes Partner</strong> - Platforma Networkingu Regionalnej Izby Przedsiębiorców</p>
<p><a href="https://nordabiznes.pl">nordabiznes.pl</a></p> body_html = _email_v3_wrap('Witamy!', 'Norda Biznes Partner', content)
</div>
</div>
</body>
</html>
"""
return send_email( return send_email(
to=[email], to=[email],

View File

@ -293,43 +293,35 @@ Aby przestac obserwowac ten watek: {unsubscribe_url}
Norda Biznes Partner - https://nordabiznes.pl Norda Biznes Partner - https://nordabiznes.pl
""" """
body_html = f"""<!DOCTYPE html> from email_service import _email_v3_wrap
<html>
<head> content = f'''
<meta charset="UTF-8"> <p style="margin:0 0 16px; color:#1e293b; font-size:16px;">Cześć <strong>{subscriber.get('name', '')}</strong>!</p>
<style> <p style="margin:0 0 24px; color:#475569; font-size:15px; line-height:1.5;"><strong>{replier_name}</strong> odpowiedział w temacie, który obserwujesz:</p>
body {{ font-family: 'Inter', Arial, sans-serif; line-height: 1.6; color: #1e293b; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; background: #f8fafc; }} <table width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc; border-radius:10px; border: 1px solid #e2e8f0; margin-bottom:24px;">
.header {{ background-color: #1e3a8a; background: linear-gradient(135deg, #1e40af, #1e3a8a); color: white; padding: 24px; text-align: center; border-radius: 8px 8px 0 0; }} <tr><td style="padding: 16px 20px;">
.header h1 {{ margin: 0; font-size: 22px; font-weight: 700; }} <p style="margin:0 0 4px; color:#64748b; font-size:12px; text-transform:uppercase; letter-spacing:0.5px;">Temat</p>
.content {{ background: white; padding: 30px; border-radius: 0 0 8px 8px; }} <p style="margin:0; color:#1e3a8a; font-size:17px; font-weight:600;">{topic_title}</p>
.quote {{ background: #f1f5f9; border-left: 4px solid #2563eb; padding: 15px; margin: 20px 0; border-radius: 0 8px 8px 0; color: #475569; font-style: italic; }} </td></tr>
.button {{ display: inline-block; padding: 14px 32px; background: #2563eb; color: white; text-decoration: none; border-radius: 8px; margin: 20px 0; font-weight: 600; }} </table>
.footer {{ text-align: center; padding: 20px; color: #94a3b8; font-size: 0.85em; }}
.footer a {{ color: #94a3b8; }} <table width="100%" cellpadding="0" cellspacing="0" style="background:#f1f5f9; border-left: 4px solid #2563eb; border-radius: 0 8px 8px 0; margin-bottom:28px;">
</style> <tr><td style="padding: 16px 20px;">
</head> <p style="margin:0; color:#475569; font-size:14px; font-style:italic; line-height:1.6;">{preview}</p>
<body> </td></tr>
<div class="container"> </table>
<div class="header">
<h1>Nowa odpowiedz na forum</h1> <table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:20px;">
</div> <tr><td align="center" style="padding: 8px 0;">
<div class="content"> <a href="{topic_url}" style="display:inline-block; padding:16px 40px; background: linear-gradient(135deg, #1e3a8a, #172554); color:#ffffff; text-decoration:none; border-radius:8px; font-size:15px; font-weight:600;">Zobacz odpowiedź &rarr;</a>
<p>Czesc {subscriber.get('name', '')}!</p> </td></tr>
<p><strong>{replier_name}</strong> odpowiedzial w temacie, ktory obserwujesz:</p> <tr><td align="center" style="padding: 8px 0;">
<h3 style="color: #1e3a8a;">{topic_title}</h3> <a href="{unsubscribe_url}" style="color:#94a3b8; font-size:13px; text-decoration:none;">Przestań obserwować ten wątek</a>
<div class="quote">{preview}</div> </td></tr>
<p style="text-align: center;"> </table>'''
<a href="{topic_url}" class="button">Zobacz odpowiedz</a>
</p> body_html = _email_v3_wrap('Nowa odpowiedź na forum', 'Norda Biznes Partner', content)
</div>
<div class="footer">
<p>Norda Biznes Partner - Platforma Networkingu</p>
<p><a href="{unsubscribe_url}">Przestam obserwowac ten watek</a></p>
</div>
</div>
</body>
</html>"""
try: try:
result = send_email( result = send_email(