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
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:
parent
3a7faa782b
commit
be2c3e030b
@ -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()
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user