feat: clickable member names, auto-linked URLs, redesigned calendar section
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

- Member names in event description link to person profiles
- URLs auto-linked (nordabiznes.pl, https://... etc)
- Calendar add section: blue gradient card with Google/Outlook buttons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-12 10:23:33 +01:00
parent 3a7faa782b
commit be2c3e030b
2 changed files with 136 additions and 23 deletions

View File

@ -120,6 +120,45 @@ def index():
db.close()
def _enrich_event_description(db, html):
"""Enrich event description: link member names and auto-link URLs."""
import re
from markupsafe import Markup
from flask import url_for as flask_url_for
from database import User
# 1. Auto-link bare URLs (not already inside href="...")
def linkify_urls(text):
url_pattern = r'(?<!["\'>=/])(https?://[^\s<>"\']+|(?<!\w)(?:www\.)[^\s<>"\']+|(?<!\w)nordabiznes\.pl[^\s<>"\']*)'
def url_replacer(m):
url = m.group(0)
href = url if url.startswith('http') else 'https://' + url
return f'<a href="{href}" target="_blank" style="color:var(--primary);font-weight:500;">{url}</a>'
return re.sub(url_pattern, url_replacer, text)
# 2. Find portal members with person_id (for clickable names)
members = db.query(User.name, User.person_id).filter(
User.person_id.isnot(None),
User.name.isnot(None),
).all()
# Sort by name length descending (longer names first to avoid partial matches)
members = sorted(members, key=lambda m: len(m.name), reverse=True)
# Apply URL linkification first
html = linkify_urls(html)
# 3. Replace member names with links (not inside existing tags)
for member in members:
name = member.name
person_url = flask_url_for('person_detail', person_id=member.person_id)
pattern = r'(?<!["\w>])(' + re.escape(name) + r')(?!["\w<])'
link = f'<a href="{person_url}" style="color:var(--primary);font-weight:600;text-decoration:none;border-bottom:1px dashed var(--primary);" title="Zobacz profil">{name}</a>'
html = re.sub(pattern, link, html)
return Markup(html)
@bp.route('/<int:event_id>', endpoint='calendar_event')
@login_required
def event(event_id):
@ -144,8 +183,8 @@ def event(event_id):
# Find speaker's person_id by matching name
speaker_person_id = None
from database import User
if event.speaker_name:
from database import User
speaker_user = db.query(User).filter(
User.name == event.speaker_name,
User.person_id.isnot(None)
@ -153,10 +192,16 @@ def event(event_id):
if speaker_user:
speaker_person_id = speaker_user.person_id
# Enrich description: linkify member names and URLs
enriched_description = event.description or ''
if enriched_description:
enriched_description = _enrich_event_description(db, enriched_description)
return render_template('calendar/event.html',
event=event,
user_attending=user_attending,
speaker_person_id=speaker_person_id,
enriched_description=enriched_description,
)
finally:
db.close()

View File

@ -249,40 +249,94 @@
}
.calendar-add {
display: flex;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-md) var(--spacing-lg);
background: linear-gradient(135deg, #f0f9ff, #eff6ff);
border: 1px solid #bfdbfe;
border-radius: var(--radius-lg);
margin-bottom: var(--spacing-xl);
}
.calendar-add-icon {
width: 40px;
height: 40px;
background: var(--primary);
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.calendar-add-icon svg {
width: 20px;
height: 20px;
color: white;
}
.calendar-add-label {
font-size: var(--font-size-sm);
font-weight: 600;
color: #1e40af;
}
.calendar-add-buttons {
display: flex;
gap: var(--spacing-sm);
flex-wrap: wrap;
margin-top: var(--spacing-md);
padding-top: var(--spacing-md);
border-top: 1px solid var(--border-color, #e5e7eb);
margin-left: auto;
}
.calendar-add-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
padding: 8px 16px;
border-radius: var(--radius);
font-size: var(--font-size-sm);
font-weight: 500;
text-decoration: none;
transition: var(--transition);
border: 1px solid var(--border-color, #e5e7eb);
background: var(--surface);
color: var(--text-primary);
border: none;
cursor: pointer;
color: white;
}
.calendar-add-btn:hover {
background: var(--background);
border-color: var(--primary);
color: var(--primary);
.calendar-add-btn.google {
background: #4285f4;
}
.calendar-add-btn.google:hover {
background: #3367d6;
}
.calendar-add-btn.outlook {
background: #0078d4;
}
.calendar-add-btn.outlook:hover {
background: #106ebe;
}
.calendar-add-btn svg {
width: 16px;
height: 16px;
}
@media (max-width: 640px) {
.calendar-add {
flex-direction: column;
align-items: stretch;
gap: var(--spacing-sm);
}
.calendar-add-buttons {
margin-left: 0;
}
.calendar-add-icon {
display: none;
}
}
</style>
{% endblock %}
@ -378,15 +432,25 @@
{% if event.time_start and not event.is_past %}
<div class="calendar-add">
<span style="font-size: var(--font-size-sm); color: var(--text-secondary); align-self: center;">Dodaj do kalendarza:</span>
<a href="#" class="calendar-add-btn" onclick="addToGoogleCalendar(); return false;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.5 3h-15A1.5 1.5 0 003 4.5v15A1.5 1.5 0 004.5 21h15a1.5 1.5 0 001.5-1.5v-15A1.5 1.5 0 0019.5 3zM12 17.25a.75.75 0 01-.75-.75v-3.75H7.5a.75.75 0 010-1.5h3.75V7.5a.75.75 0 011.5 0v3.75h3.75a.75.75 0 010 1.5h-3.75v3.75a.75.75 0 01-.75.75z"/></svg>
Google Calendar
</a>
<a href="#" class="calendar-add-btn" onclick="downloadICS(); return false;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.5 3h-15A1.5 1.5 0 003 4.5v15A1.5 1.5 0 004.5 21h15a1.5 1.5 0 001.5-1.5v-15A1.5 1.5 0 0019.5 3zM12 17.25a.75.75 0 01-.75-.75v-3.75H7.5a.75.75 0 010-1.5h3.75V7.5a.75.75 0 011.5 0v3.75h3.75a.75.75 0 010 1.5h-3.75v3.75a.75.75 0 01-.75.75z"/></svg>
Outlook / ICS
</a>
<div class="calendar-add-icon">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
</div>
<div class="calendar-add-label">Dodaj do kalendarza</div>
<div class="calendar-add-buttons">
<a href="#" class="calendar-add-btn google" onclick="addToGoogleCalendar(); return false;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.94-.49-7-3.85-7-7.93s3.05-7.44 7-7.93v2.02C8.16 6.57 6 9.03 6 12s2.16 5.43 5 5.91v2.02zm2 0V17.9c2.84-.48 5-2.94 5-5.9s-2.16-5.43-5-5.91V4.07c3.94.49 7 3.85 7 7.93s-3.06 7.44-7 7.93z"/></svg>
Google
</a>
<a href="#" class="calendar-add-btn outlook" onclick="downloadICS(); return false;">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zm0-12H5V6h14v2z"/></svg>
Outlook / ICS
</a>
</div>
</div>
{% endif %}
@ -396,7 +460,11 @@
</div>
{% endif %}
{% if event.description %}
{% if enriched_description %}
<div class="event-description">
{{ enriched_description }}
</div>
{% elif event.description %}
<div class="event-description">
{{ event.description|safe }}
</div>