nordabiz/templates/calendar/event.html
Maciej Pienczyn db660d9c05
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
style(calendar): Add white-space pre-line for plain text descriptions
Ensures newlines in plain text event descriptions render as line
breaks, maintaining compatibility with both HTML and plain text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 15:20:05 +01:00

432 lines
14 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}{{ event.title }} - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.event-header {
margin-bottom: var(--spacing-xl);
}
.event-header h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.event-detail {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
box-shadow: var(--shadow);
margin-bottom: var(--spacing-xl);
}
.event-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.info-item {
display: flex;
align-items: flex-start;
gap: var(--spacing-sm);
}
.info-item svg {
width: 20px;
height: 20px;
color: var(--primary);
flex-shrink: 0;
margin-top: 2px;
}
.info-label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.info-value {
font-weight: 500;
color: var(--text-primary);
}
.event-description {
margin-bottom: var(--spacing-xl);
line-height: 1.8;
color: var(--text-primary);
font-size: 1.05rem;
white-space: pre-line;
}
.event-description p {
margin-bottom: var(--spacing-md);
}
.event-description p:last-child {
margin-bottom: 0;
}
.event-callout {
display: flex;
align-items: flex-start;
gap: var(--spacing-md);
padding: var(--spacing-lg);
background: linear-gradient(135deg, #f0fdf4, #ecfdf5);
border: 1px solid #bbf7d0;
border-radius: var(--radius-lg);
margin-top: var(--spacing-lg);
}
.event-callout svg {
width: 22px;
height: 22px;
color: #16a34a;
flex-shrink: 0;
margin-top: 2px;
}
.event-callout strong {
color: #15803d;
}
.event-callout small {
display: block;
margin-top: var(--spacing-xs);
color: var(--text-secondary);
}
.rsvp-section {
display: flex;
align-items: center;
gap: var(--spacing-lg);
padding: var(--spacing-lg);
background: var(--background);
border-radius: var(--radius);
}
.attendees-section {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
box-shadow: var(--shadow);
}
.attendees-section h2 {
font-size: var(--font-size-xl);
margin-bottom: var(--spacing-lg);
}
.attendees-list {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.attendee-badge {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-xs);
background: var(--background);
border-radius: var(--radius);
font-size: var(--font-size-sm);
}
/* Unverified person - blue */
.attendee-name {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
background: #dbeafe;
color: #1e40af;
border-radius: var(--radius);
font-weight: 500;
font-size: var(--font-size-sm);
text-decoration: none;
transition: var(--transition);
}
/* Verified person - green */
.attendee-name.verified {
background: #dcfce7;
color: #166534;
}
a.attendee-name:hover {
background: #bfdbfe;
transform: translateY(-1px);
}
a.attendee-name.verified:hover {
background: #bbf7d0;
}
.attendee-name svg {
width: 14px;
height: 14px;
}
.attendee-company {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
background: #fee2e2;
color: #991b1b;
border-radius: var(--radius);
font-weight: 500;
font-size: var(--font-size-sm);
text-decoration: none;
transition: var(--transition);
}
.attendee-company:hover {
background: #fecaca;
transform: translateY(-1px);
}
.attendee-company svg {
width: 14px;
height: 14px;
}
.back-link {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
color: var(--text-secondary);
text-decoration: none;
margin-bottom: var(--spacing-lg);
}
.back-link:hover {
color: var(--primary);
}
#rsvp-btn.attending {
background: var(--success);
}
</style>
{% endblock %}
{% block content %}
<a href="{{ url_for('calendar.calendar_index') }}" class="back-link">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
Powrót do kalendarza
</a>
<div class="event-header">
<h1>{{ event.title }}</h1>
</div>
<div class="event-detail">
<div class="event-info-grid">
<div class="info-item">
<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="info-label">Data</div>
<div class="info-value">{{ event.event_date.strftime('%d.%m.%Y') }}</div>
</div>
</div>
{% if event.time_start %}
<div class="info-item">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<div>
<div class="info-label">Godzina</div>
<div class="info-value">{{ event.time_start.strftime('%H:%M') }}{% if event.time_end %} - {{ event.time_end.strftime('%H:%M') }}{% endif %}</div>
</div>
</div>
{% endif %}
{% if event.location %}
<div class="info-item">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
<div>
<div class="info-label">Miejsce</div>
<div class="info-value">
{% if event.location_url %}
<a href="{{ event.location_url }}" target="_blank">{{ event.location }}</a>
{% else %}
{{ event.location }}
{% endif %}
</div>
</div>
</div>
{% endif %}
{% if event.speaker_name %}
<div class="info-item">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<div>
<div class="info-label">Prelegent</div>
<div class="info-value">{{ event.speaker_name }}</div>
</div>
</div>
{% endif %}
</div>
{% if event.image_url %}
<div style="margin-bottom: var(--spacing-xl); border-radius: var(--radius-lg); overflow: hidden;">
<img src="{{ event.image_url }}" alt="{{ event.title }}" style="width: 100%; height: auto; display: block;">
</div>
{% endif %}
{% if event.description %}
<div class="event-description">
{{ event.description|safe }}
</div>
{% endif %}
{% if not event.is_past %}
{% if event.can_user_attend(current_user) %}
<div class="rsvp-section">
<div>
<strong>Chcesz wziąć udział?</strong>
<p class="text-muted" style="margin: 0;">{{ event.attendee_count }} osób już się zapisało{% if event.max_attendees %} (limit: {{ event.max_attendees }}){% endif %}</p>
</div>
<button id="rsvp-btn" class="btn {% if user_attending %}btn-secondary attending{% else %}btn-primary{% endif %}" onclick="toggleRSVP()">
{% if user_attending %}Wypisz się{% else %}Wezmę udział{% endif %}
</button>
</div>
{% elif event.access_level == 'rada_only' %}
<div class="rsvp-section" style="background: #fef3c7; border: 1px solid #fde68a;">
<svg width="24" height="24" fill="none" stroke="#92400e" stroke-width="2" viewBox="0 0 24 24" style="flex-shrink: 0;">
<path d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
<div>
<strong style="color: #92400e;">Wydarzenie tylko dla Rady Izby</strong>
<p class="text-muted" style="margin: 0; color: #a16207;">Zapisy są dostępne wyłącznie dla członków Rady Izby NORDA.</p>
</div>
</div>
{% endif %}
{% else %}
<div class="rsvp-section" style="background: var(--border);">
<p class="text-muted" style="margin: 0;">To wydarzenie już się odbyło.{% if event.can_user_see_attendees(current_user) %} {{ event.attendee_count }} osób brało udział.{% endif %}</p>
</div>
{% endif %}
</div>
{% if event.attendees and event.can_user_see_attendees(current_user) %}
<div class="attendees-section">
<h2>Uczestnicy ({{ event.attendee_count }})</h2>
<div class="attendees-list">
{% for attendee in event.attendees|sort(attribute='user.name') %}
<div class="attendee-badge">
{# Person badge - green if verified (has person_id), blue if not #}
{% if attendee.user.person_id %}
<a href="{{ url_for('person_detail', person_id=attendee.user.person_id) }}" class="attendee-name verified">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
{{ attendee.user.name or attendee.user.email.split('@')[0] }}
</a>
{% else %}
<span class="attendee-name">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
{{ attendee.user.name or attendee.user.email.split('@')[0] }}
</span>
{% endif %}
{# Company badge - always link to company profile #}
{% if attendee.user.company %}
<a href="{{ url_for('company_detail_by_slug', slug=attendee.user.company.slug) }}" class="attendee-company">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M19 21V5a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v5m-4 0h4"></path>
</svg>
{{ attendee.user.company.name }}
</a>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1100; display: flex; flex-direction: column; gap: 10px;"></div>
<style>
.toast { padding: 12px 20px; border-radius: var(--radius); background: var(--surface); border-left: 4px solid var(--primary); box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 10px; animation: toastIn 0.3s ease; }
.toast.success { border-left-color: var(--success); }
.toast.error { border-left-color: var(--error); }
@keyframes toastIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
</style>
{% endblock %}
{% block extra_js %}
const csrfToken = '{{ csrf_token() }}';
function showToast(message, type = 'info', duration = 4000) {
const container = document.getElementById('toastContainer');
const icons = { success: '✓', error: '✕', warning: '⚠', info: '' };
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `<span style="font-size:1.2em">${icons[type]||''}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration);
}
async function toggleRSVP() {
const btn = document.getElementById('rsvp-btn');
btn.disabled = true;
try {
const response = await fetch('{{ url_for("calendar.calendar_rsvp", event_id=event.id) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
if (data.action === 'added') {
btn.textContent = 'Wypisz się';
btn.classList.remove('btn-primary');
btn.classList.add('btn-secondary', 'attending');
showToast('Zapisano na wydarzenie!', 'success');
} else {
btn.textContent = 'Wezmę udział';
btn.classList.remove('btn-secondary', 'attending');
btn.classList.add('btn-primary');
showToast('Wypisano z wydarzenia', 'info');
}
// Refresh page to update attendees list
setTimeout(() => location.reload(), 1000);
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
btn.disabled = false;
}
{% endblock %}