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
ICS format requires plain text (not HTML) in DESCRIPTION field and lines must not exceed 75 octets per RFC 5545. This fixes "preview unavailable" when importing ICS into Outlook/macOS Calendar. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
746 lines
25 KiB
HTML
Executable File
746 lines
25 KiB
HTML
Executable File
{% 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.7;
|
||
color: var(--text-primary);
|
||
font-size: 1.05rem;
|
||
}
|
||
|
||
.event-description p {
|
||
margin-bottom: var(--spacing-md);
|
||
}
|
||
|
||
.event-description p:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.event-description h3 {
|
||
font-size: var(--font-size-lg);
|
||
margin: var(--spacing-lg) 0 var(--spacing-sm);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.event-description ul {
|
||
margin: 0 0 var(--spacing-md) var(--spacing-lg);
|
||
padding: 0;
|
||
}
|
||
|
||
.event-description li {
|
||
margin-bottom: var(--spacing-xs);
|
||
}
|
||
|
||
.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);
|
||
white-space: normal;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.event-logo {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-md);
|
||
margin-bottom: var(--spacing-lg);
|
||
}
|
||
|
||
.event-logo img {
|
||
height: 48px;
|
||
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;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.speaker-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.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);
|
||
margin-left: auto;
|
||
}
|
||
|
||
.calendar-add-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 16px;
|
||
border-radius: var(--radius);
|
||
font-size: var(--font-size-sm);
|
||
font-weight: 500;
|
||
text-decoration: none;
|
||
transition: var(--transition);
|
||
border: none;
|
||
cursor: pointer;
|
||
color: white;
|
||
}
|
||
|
||
.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 %}
|
||
|
||
{% 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">
|
||
{% if event.is_featured %}
|
||
<div class="event-logo">
|
||
<img src="{{ url_for('static', filename='img/norda-logo.svg') }}" alt="Norda Biznes">
|
||
</div>
|
||
{% endif %}
|
||
<h1>{{ event.title }}
|
||
{% if event.access_level == 'admin_only' %}
|
||
<span style="display:inline-block;background:#ef4444;color:#fff;font-size:12px;padding:2px 8px;border-radius:4px;font-weight:600;vertical-align:middle;">UKRYTE</span>
|
||
{% elif event.access_level == 'rada_only' %}
|
||
<span style="display:inline-block;background:#f59e0b;color:#92400e;font-size:12px;padding:2px 8px;border-radius:4px;font-weight:600;vertical-align:middle;">IZBA</span>
|
||
{% endif %}
|
||
</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" style="color:var(--primary);">{{ event.location }}</a>
|
||
{% elif event.location and event.location not in ['Do ustalenia', 'Online'] and 'do ustalenia' not in event.location|lower %}
|
||
<a href="https://www.google.com/maps/search/{{ event.location|urlencode }}" target="_blank" style="color:var(--primary);">{{ 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">
|
||
{% if speaker_user_id %}
|
||
<a href="{{ url_for('public.user_profile', user_id=speaker_user_id) }}" class="person-link">{{ event.speaker_name }}</a>
|
||
{% elif speaker_company_slug %}
|
||
<a href="{{ url_for('public.company_detail_by_slug', slug=speaker_company_slug) }}" class="company-link">{{ event.speaker_name }}</a>
|
||
{% else %}
|
||
{{ event.speaker_name }}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if event.time_start and not event.is_past %}
|
||
<div class="calendar-add">
|
||
<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 %}
|
||
|
||
{% 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 enriched_description %}
|
||
<div class="event-description">
|
||
{{ enriched_description }}
|
||
</div>
|
||
{% elif event.description %}
|
||
<div class="event-description">
|
||
{{ event.description|safe }}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if event.attachment_filename %}
|
||
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-lg); background: linear-gradient(135deg, #fef3c7, #fefce8); border: 1px solid #fde68a; border-radius: var(--radius-lg); margin-bottom: var(--spacing-xl);">
|
||
<svg width="24" height="24" fill="none" stroke="#92400e" stroke-width="2" viewBox="0 0 24 24" style="flex-shrink: 0;">
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||
<polyline points="14 2 14 8 20 8"></polyline>
|
||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||
</svg>
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 600; color: #92400e;">Załącznik</div>
|
||
<a href="{{ url_for('static', filename=event.attachment_path.replace('static/', '')) }}" target="_blank" style="color: #b45309; text-decoration: underline;">{{ event.attachment_filename }}</a>
|
||
</div>
|
||
</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 - always clickable: person profile if person_id, user profile otherwise #}
|
||
{% if attendee.user.person_id %}
|
||
<a href="{{ url_for('public.person_detail', person_id=attendee.user.person_id) }}" class="attendee-name verified">
|
||
{% else %}
|
||
<a href="{{ url_for('public.user_profile', user_id=attendee.user.id) }}" class="attendee-name verified">
|
||
{% endif %}
|
||
<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>
|
||
|
||
{# Company badge - always link to company profile #}
|
||
{% if attendee.user.company %}
|
||
<a href="{{ url_for('public.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;
|
||
}
|
||
|
||
/* --- Add to Calendar functions --- */
|
||
function stripHtml(html) {
|
||
const tmp = document.createElement('div');
|
||
tmp.innerHTML = html;
|
||
return tmp.textContent || tmp.innerText || '';
|
||
}
|
||
|
||
function foldIcsLine(line) {
|
||
// ICS spec: lines max 75 octets, continuation lines start with space
|
||
const parts = [];
|
||
while (line.length > 75) {
|
||
parts.push(line.substring(0, 75));
|
||
line = ' ' + line.substring(75);
|
||
}
|
||
parts.push(line);
|
||
return parts.join('\r\n');
|
||
}
|
||
|
||
const _evt = {
|
||
title: {{ event.title|tojson }},
|
||
date: '{{ event.event_date.strftime("%Y%m%d") }}',
|
||
start: '{{ event.time_start.strftime("%H%M") if event.time_start else "0000" }}',
|
||
end: '{{ event.time_end.strftime("%H%M") if event.time_end else "" }}',
|
||
location: {{ (event.location or '')|tojson }},
|
||
speaker: {{ (event.speaker_name or '')|tojson }},
|
||
description: {{ (event.description or '')|tojson }},
|
||
organizerName: {{ (event.organizer_name or 'Norda Biznes')|tojson }},
|
||
organizerEmail: {{ (event.organizer_email or 'biuro@norda-biznes.info')|tojson }},
|
||
url: window.location.href,
|
||
};
|
||
|
||
function addToGoogleCalendar() {
|
||
const start = _evt.date + 'T' + _evt.start + '00';
|
||
const end = _evt.end ? (_evt.date + 'T' + _evt.end + '00') : '';
|
||
const details = [
|
||
_evt.speaker ? 'Prowadzący: ' + _evt.speaker : '',
|
||
_evt.description,
|
||
'',
|
||
'Szczegóły: ' + _evt.url,
|
||
].filter(Boolean).join('\n');
|
||
const params = new URLSearchParams({
|
||
action: 'TEMPLATE',
|
||
text: _evt.title,
|
||
dates: start + '/' + (end || start),
|
||
location: _evt.location,
|
||
details: details,
|
||
ctz: 'Europe/Warsaw',
|
||
});
|
||
window.open('https://calendar.google.com/calendar/render?' + params, '_blank');
|
||
}
|
||
|
||
function downloadICS() {
|
||
const start = _evt.date + 'T' + _evt.start + '00';
|
||
const end = _evt.end ? (_evt.date + 'T' + _evt.end + '00') : (_evt.date + 'T' + _evt.start + '00');
|
||
const uid = 'nordabiz-event-{{ event.id }}@nordabiznes.pl';
|
||
const now = new Date().toISOString().replace(/[-:]/g,'').split('.')[0] + 'Z';
|
||
const plainDesc = stripHtml(_evt.description);
|
||
const descParts = [
|
||
_evt.speaker ? 'Prowadzący: ' + _evt.speaker : '',
|
||
plainDesc,
|
||
'',
|
||
'Szczegóły: ' + _evt.url,
|
||
].filter(Boolean);
|
||
const desc = descParts.join('\\n');
|
||
const lines = [
|
||
'BEGIN:VCALENDAR',
|
||
'VERSION:2.0',
|
||
'PRODID:-//NordaBiznes//PL',
|
||
'BEGIN:VEVENT',
|
||
'UID:' + uid,
|
||
'DTSTAMP:' + now,
|
||
'DTSTART;TZID=Europe/Warsaw:' + start,
|
||
'DTEND;TZID=Europe/Warsaw:' + end,
|
||
'SUMMARY:' + _evt.title,
|
||
'LOCATION:' + _evt.location,
|
||
'DESCRIPTION:' + desc,
|
||
'URL:' + _evt.url,
|
||
'ORGANIZER;CN=' + _evt.organizerName + ':mailto:' + _evt.organizerEmail,
|
||
'BEGIN:VALARM',
|
||
'TRIGGER:-P1D',
|
||
'ACTION:DISPLAY',
|
||
'DESCRIPTION:Jutro: ' + _evt.title,
|
||
'END:VALARM',
|
||
'END:VEVENT',
|
||
'END:VCALENDAR',
|
||
];
|
||
const ics = lines.map(foldIcsLine).join('\r\n');
|
||
const blob = new Blob([ics], { type: 'text/calendar;charset=utf-8' });
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = _evt.title.substring(0, 50).replace(/[^a-zA-Z0-9ąćęłńóśźżĄĆĘŁŃÓŚŹŻ ]/g, '') + '.ics';
|
||
a.click();
|
||
URL.revokeObjectURL(a.href);
|
||
}
|
||
{% endblock %}
|