feat(calendar): pill badge links for persons and companies in event details
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

Match NordaGPT chat visual style — person names show as green pill badges,
company names as orange pill badges. Enriches event description with
auto-linked companies (not just persons). Speaker name also uses pill badges.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-17 14:41:48 +01:00
parent 81c839ab5a
commit 4f44c59a80
2 changed files with 76 additions and 12 deletions

View File

@ -138,20 +138,32 @@ def _enrich_event_description(db, html):
paragraphs = html.split('\n\n')
html = ''.join(f'<p>{p.replace(chr(10), "<br>")}</p>' for p in paragraphs if p.strip())
# Build replacement maps
# Build replacement maps — persons
members = db.query(User.name, User.person_id).filter(
User.person_id.isnot(None),
User.name.isnot(None),
).all()
# Sort longest first to avoid partial matches
members = sorted(members, key=lambda m: len(m.name), reverse=True)
member_map = {}
person_map = {}
for m in members:
member_map[m.name] = flask_url_for('person_detail', person_id=m.person_id)
person_map[m.name] = flask_url_for('person_detail', person_id=m.person_id)
# Build replacement maps — companies
from database import Company
companies = db.query(Company.name, Company.slug).filter(
Company.slug.isnot(None),
Company.name.isnot(None),
Company.status == 'active',
).all()
companies = sorted(companies, key=lambda c: len(c.name), reverse=True)
company_map = {}
for c in companies:
company_map[c.name] = flask_url_for('company_detail', slug=c.slug)
def enrich_text_node(text):
"""Apply member linking and URL linkification to a plain text fragment."""
"""Apply person/company linking and URL linkification to a plain text fragment."""
# 1. Auto-link URLs
url_pattern = r'(https?://[^\s<>"\']+|(?<!\w)www\.[^\s<>"\']+|(?<!\w)nordabiznes\.pl[^\s<>"\']*)'
def url_replacer(m):
@ -160,10 +172,16 @@ def _enrich_event_description(db, html):
return f'<a href="{href}" target="_blank" style="color:var(--primary);font-weight:500;">{url}</a>'
text = re.sub(url_pattern, url_replacer, text)
# 2. Link member names (whole word match)
for name, url in member_map.items():
# 2. Link person names (pill badge — green)
for name, url in person_map.items():
pattern = r'\b' + re.escape(name) + r'\b'
link = f'<a href="{url}" style="color:var(--primary);font-weight:600;text-decoration:none;border-bottom:1px dashed var(--primary);" title="Zobacz profil">{name}</a>'
link = f'<a href="{url}" class="person-link" title="Zobacz profil">{name}</a>'
text = re.sub(pattern, link, text)
# 3. Link company names (pill badge — orange)
for name, url in company_map.items():
pattern = r'\b' + re.escape(name) + r'\b'
link = f'<a href="{url}" class="company-link" title="Zobacz firmę">{name}</a>'
text = re.sub(pattern, link, text)
return text
@ -229,9 +247,10 @@ def event(event_id):
EventAttendee.user_id == current_user.id
).first()
# Find speaker's person_id by matching name
# Find speaker's person_id or company by matching name
speaker_person_id = None
from database import User
speaker_company_slug = None
from database import User, Company
if event.speaker_name:
speaker_user = db.query(User).filter(
User.name == event.speaker_name,
@ -239,8 +258,16 @@ def event(event_id):
).first()
if speaker_user:
speaker_person_id = speaker_user.person_id
else:
# Try matching as company name
speaker_company = db.query(Company).filter(
Company.name == event.speaker_name,
Company.status == 'active',
).first()
if speaker_company:
speaker_company_slug = speaker_company.slug
# Enrich description: linkify member names and URLs
# Enrich description: linkify member names, companies and URLs
enriched_description = event.description or ''
if enriched_description:
enriched_description = _enrich_event_description(db, enriched_description)
@ -249,6 +276,7 @@ def event(event_id):
event=event,
user_attending=user_attending,
speaker_person_id=speaker_person_id,
speaker_company_slug=speaker_company_slug,
enriched_description=enriched_description,
)
finally:

View File

@ -238,6 +238,40 @@
width: auto;
}
/* Pill badge links — spójne z NordaGPT chat */
.person-link, .company-link {
display: inline-block;
padding: 2px 10px;
margin: 1px 2px;
border-radius: 12px;
text-decoration: none;
font-weight: 600;
font-size: 0.95em;
transition: var(--transition);
}
.person-link {
background: #ecfdf5;
color: #047857;
}
.person-link:hover {
background: #d1fae5;
color: #065f46;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(4, 120, 87, 0.2);
}
.company-link {
background: #fff7ed;
color: #c2410c;
}
.company-link:hover {
background: #ffedd5;
color: #9a3412;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(194, 65, 12, 0.2);
}
.speaker-link {
color: var(--primary);
text-decoration: none;
@ -422,7 +456,9 @@
<div class="info-label">Prelegent</div>
<div class="info-value">
{% if speaker_person_id %}
<a href="{{ url_for('person_detail', person_id=speaker_person_id) }}" class="speaker-link">{{ event.speaker_name }}</a>
<a href="{{ url_for('person_detail', person_id=speaker_person_id) }}" class="person-link">{{ event.speaker_name }}</a>
{% elif speaker_company_slug %}
<a href="{{ url_for('company_detail', slug=speaker_company_slug) }}" class="company-link">{{ event.speaker_name }}</a>
{% else %}
{{ event.speaker_name }}
{% endif %}