nordabiz/templates/calendar/event.html
Maciej Pienczyn 66856a697d refactor(phase1): Extract blueprints for reports, contacts, classifieds, calendar
Phase 1 of app.py refactoring - reducing from ~14,455 to ~13,699 lines.

New structure:
- blueprints/reports/ - 4 routes (/raporty/*)
- blueprints/community/contacts/ - 6 routes (/kontakty/*)
- blueprints/community/classifieds/ - 4 routes (/tablica/*)
- blueprints/community/calendar/ - 3 routes (/kalendarz/*)
- utils/ - decorators, helpers, notifications, analytics
- extensions.py - Flask extensions (csrf, login_manager, limiter)
- config.py - environment configurations

Updated templates with blueprint-prefixed url_for() calls.

⚠️ DO NOT DEPLOY before presentation on 2026-01-30 19:00

Tested on DEV: all endpoints working correctly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 10:10:45 +01:00

375 lines
12 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 Hub{% 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.7;
color: var(--text-primary);
}
.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.description %}
<div class="event-description">
{{ event.description|safe }}
</div>
{% endif %}
{% if not event.is_past %}
<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>
{% else %}
<div class="rsvp-section" style="background: var(--border);">
<p class="text-muted" style="margin: 0;">To wydarzenie już się odbyło. {{ event.attendee_count }} osób brało udział.</p>
</div>
{% endif %}
</div>
{% if event.attendees %}
<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 %}