nordabiz/templates/calendar/index.html
Maciej Pienczyn f8fdb6a106
Some checks are pending
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Fix desktop calendar grid — show event titles instead of empty colored bars
Event text was removed when switching to div+modal. Added it back as
<span class="calendar-event-text"> visible on desktop, hidden on mobile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 16:54:50 +01:00

977 lines
36 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 %}Kalendarz - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.calendar-header {
margin-bottom: var(--spacing-lg);
}
.calendar-header h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
/* Toolbar z przyciskami widoku */
.calendar-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xl);
flex-wrap: wrap;
gap: var(--spacing-md);
background: var(--surface);
padding: var(--spacing-md);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.view-toggle {
display: flex;
gap: 2px;
background: var(--border);
border-radius: var(--radius);
padding: 2px;
}
.view-toggle a {
padding: var(--spacing-sm) var(--spacing-md);
text-decoration: none;
color: var(--text-secondary);
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
font-weight: 500;
transition: var(--transition);
}
.view-toggle a.active {
background: var(--primary);
color: white;
}
.view-toggle a:not(.active):hover {
background: var(--surface);
color: var(--text-primary);
}
.month-nav {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.month-nav a {
padding: var(--spacing-xs) var(--spacing-sm);
text-decoration: none;
color: var(--text-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
transition: var(--transition);
}
.month-nav a:hover {
background: var(--surface-hover, #f3f4f6);
color: var(--text-primary);
}
.current-month {
font-weight: 600;
font-size: var(--font-size-lg);
min-width: 160px;
text-align: center;
color: var(--text-primary);
}
/* Siatka kalendarza */
.calendar-grid {
background: var(--surface);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow);
}
.calendar-header-row {
display: grid;
grid-template-columns: repeat(7, 1fr);
}
.day-header {
padding: var(--spacing-sm);
text-align: center;
font-weight: 600;
font-size: var(--font-size-sm);
background: var(--primary);
color: white;
}
.day-header.weekend {
background: #1e40af;
}
.calendar-week {
display: grid;
grid-template-columns: repeat(7, 1fr);
}
.calendar-day {
min-height: 110px;
padding: var(--spacing-xs);
border: 1px solid var(--border);
border-top: none;
background: var(--background);
vertical-align: top;
}
.calendar-day:not(:last-child) {
border-right: none;
}
.calendar-day.empty {
background: #f9fafb;
}
.calendar-day.today {
background: #eff6ff;
border-color: #93c5fd;
border-width: 1px;
box-shadow: inset 0 0 0 1px #93c5fd;
}
.calendar-day.weekend {
background: #fafafa;
}
.day-number {
font-weight: 600;
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-xs);
color: var(--text-secondary);
}
.calendar-day.today .day-number {
color: var(--primary);
}
.calendar-event {
display: block;
padding: 3px 6px;
margin-bottom: 3px;
border-radius: var(--radius-sm);
font-size: 11px;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: var(--transition);
}
.calendar-event:hover {
opacity: 0.8;
transform: scale(1.02);
}
.calendar-event.featured {
background: #fee2e2;
color: #991b1b;
}
.calendar-event.meeting {
background: #dbeafe;
color: #1e40af;
}
.calendar-event.networking {
background: #fef3c7;
color: #92400e;
}
.calendar-event.webinar {
background: #dcfce7;
color: #166534;
}
.calendar-event.other {
background: #f3e8ff;
color: #7c3aed;
}
.calendar-event.external {
background: #fff7ed;
color: #c2410c;
border-left: 2px solid #fb923c;
}
/* Widok listy - istniejące style */
.events-section {
margin-bottom: var(--spacing-2xl);
}
.events-section h2 {
font-size: var(--font-size-xl);
margin-bottom: var(--spacing-lg);
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.event-card {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-md);
box-shadow: var(--shadow);
display: flex;
gap: var(--spacing-lg);
transition: var(--transition);
}
.event-card:hover {
box-shadow: var(--shadow-md);
}
.event-card.featured {
border-left: 4px solid #ef4444;
}
.event-card.past {
opacity: 0.7;
}
.event-date-box {
min-width: 70px;
text-align: center;
background: var(--primary);
color: white;
border-radius: var(--radius);
padding: var(--spacing-sm);
}
.event-date-box .day {
font-size: var(--font-size-2xl);
font-weight: 700;
line-height: 1;
}
.event-date-box .month {
font-size: var(--font-size-sm);
text-transform: uppercase;
}
.event-info {
flex: 1;
}
.event-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.event-title a {
color: inherit;
text-decoration: none;
}
.event-title a:hover {
color: var(--primary);
}
.event-meta {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-md);
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
}
.event-meta span {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.event-description {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.event-actions {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.attendee-count {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.empty-state {
text-align: center;
padding: var(--spacing-2xl);
color: var(--text-secondary);
background: var(--surface);
border-radius: var(--radius-lg);
}
.badge-type {
display: inline-block;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 500;
text-transform: uppercase;
}
.badge-type.meeting { background: #dbeafe; color: #1e40af; }
.badge-type.webinar { background: #dcfce7; color: #166534; }
.badge-type.networking { background: #fef3c7; color: #92400e; }
.badge-type.other { background: #f3e8ff; color: #7c3aed; }
.badge-type.external { background: #fff7ed; color: #c2410c; }
.event-card.external-event {
border-left: 3px solid #94a3b8;
opacity: 0.92;
}
.event-card.external-event .event-date-box {
background: #64748b;
}
.external-source {
font-size: var(--font-size-xs);
color: var(--text-secondary);
font-style: italic;
}
.filter-toggle {
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-size: var(--font-size-sm);
color: var(--text-secondary);
cursor: pointer;
user-select: none;
}
.filter-toggle input { width: auto; }
.rsvp-list-btn { min-width: 110px; transition: var(--transition); }
.rsvp-list-btn.rsvp-attending {
background: #059669;
border-color: #059669;
color: white;
}
.rsvp-list-btn.rsvp-attending:hover {
background: #dc2626;
border-color: #dc2626;
}
.rsvp-list-btn.rsvp-not-attending {
background: #2563eb;
border-color: #2563eb;
color: white;
}
.rsvp-list-btn.rsvp-not-attending:hover {
background: #1d4ed8;
border-color: #1d4ed8;
}
/* Responsywność */
@media (max-width: 768px) {
.calendar-toolbar {
flex-direction: column;
align-items: stretch;
}
.month-nav {
justify-content: center;
}
.calendar-day {
min-height: 80px;
padding: 2px;
}
.calendar-event {
font-size: 10px;
padding: 2px 4px;
}
.day-header {
font-size: 9px;
padding: 2px;
letter-spacing: -0.5px;
}
.calendar-day {
min-height: 50px;
padding: 0;
display: flex;
flex-direction: column;
position: relative;
}
.day-number {
font-size: 11px;
margin: 0;
padding: 2px 4px;
position: relative;
z-index: 1;
}
/* Events fill the entire cell as colored strips */
.calendar-day .calendar-events-wrap {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.calendar-event {
padding: 0;
margin: 0;
border-radius: 0;
width: 100%;
flex: 1;
display: block;
border-left: none;
min-height: 12px;
}
.calendar-event-text {
display: none;
}
/* Single event — fills entire cell below day number */
.calendar-day.has-1-event {
padding: 0;
}
.calendar-day.has-1-event .calendar-event {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
min-height: auto;
}
.calendar-day.has-1-event .day-number {
color: inherit;
}
.calendar-day.empty { background: #f9fafb; }
.calendar-day.has-events { cursor: pointer; }
/* Day modal - bottom sheet */
.day-modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.4);
z-index: 9998;
}
.day-modal-overlay.show { display: block; }
.day-modal {
display: block;
position: fixed;
bottom: 0; left: 0; right: 0;
background: white;
border-radius: 16px 16px 0 0;
box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
z-index: 9999;
padding-bottom: env(safe-area-inset-bottom, 16px);
visibility: hidden;
transform: translateY(100%);
transition: transform 0.25s ease-out, visibility 0s 0.25s;
pointer-events: none;
max-height: 70vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.day-modal.show {
visibility: visible;
transform: translateY(0);
transition: transform 0.25s ease-out, visibility 0s 0s;
pointer-events: auto;
}
.day-modal-handle {
width: 36px; height: 4px;
background: #d1d5db; border-radius: 2px;
margin: 10px auto 4px;
}
.day-modal-title {
padding: 8px 16px 12px;
font-size: 17px; font-weight: 600;
border-bottom: 1px solid var(--border);
}
.day-modal-event {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
text-decoration: none;
color: var(--text-primary);
border-bottom: 1px solid #f1f5f9;
transition: background 0.15s;
}
.day-modal-event:active { background: #f1f5f9; }
.day-modal-event-dot {
width: 12px; height: 12px;
border-radius: 3px; flex-shrink: 0;
}
.day-modal-event-dot.featured { background: #ef4444; }
.day-modal-event-dot.meeting { background: #3b82f6; }
.day-modal-event-dot.networking { background: #f59e0b; }
.day-modal-event-dot.webinar { background: #22c55e; }
.day-modal-event-dot.other { background: #a855f7; }
.day-modal-event-dot.external { background: #fb923c; }
.day-modal-event-info { flex: 1; }
.day-modal-event-title { font-weight: 600; font-size: 15px; }
.day-modal-event-meta { font-size: 13px; color: var(--text-secondary); margin-top: 2px; }
.day-modal-event-arrow { color: #94a3b8; font-size: 18px; }
.event-card {
flex-wrap: wrap;
}
.event-actions {
width: 100%;
justify-content: flex-start;
padding-top: var(--spacing-sm);
border-top: 1px solid var(--border);
margin-top: var(--spacing-sm);
}
.calendar-header h1 {
font-size: var(--font-size-xl);
}
}
</style>
{% endblock %}
{% set pl_months = {'Jan':'sty','Feb':'lut','Mar':'mar','Apr':'kwi','May':'maj','Jun':'cze','Jul':'lip','Aug':'sie','Sep':'wrz','Oct':'paź','Nov':'lis','Dec':'gru'} %}
{% set pl_types = {'meeting':'Spotkanie','networking':'Networking','webinar':'Webinar','other':'Inne','conference':'Konferencja','workshop':'Warsztaty'} %}
{% block content %}
<div class="calendar-header">
<h1>Kalendarz wydarzeń</h1>
<div style="display: flex; align-items: center; gap: var(--spacing-md); flex-wrap: wrap;">
<p class="text-muted" style="margin: 0;">Spotkania i wydarzenia Norda Biznes</p>
<button onclick="showSubscribeModal()" style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; border-radius: var(--radius); font-size: 13px; cursor: pointer; font-weight: 500; white-space: nowrap;">
<svg width="14" height="14" 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"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
Subskrybuj kalendarz
</button>
</div>
</div>
<!-- Subscribe modal -->
<div id="subscribeModal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:9999; align-items:center; justify-content:center;">
<div style="background:white; border-radius:var(--radius-xl); padding:24px; max-width:500px; width:90%; margin:auto; position:relative; top:50%; transform:translateY(-50%);">
<button onclick="document.getElementById('subscribeModal').style.display='none'" style="position:absolute; top:12px; right:16px; background:none; border:none; font-size:20px; cursor:pointer; color:#64748b;">&times;</button>
<h3 style="margin:0 0 8px 0; font-size:18px;">Dodaj wydarzenia do kalendarza</h3>
<p style="color:#64748b; font-size:13px; line-height:1.5; margin-bottom:16px;">
Kliknij przycisk odpowiedni dla Twojego kalendarza. Wydarzenia Izby będą się automatycznie synchronizować.
</p>
<div style="display:flex; flex-direction:column; gap:10px; margin-bottom:16px;">
<a href="webcal://nordabiznes.pl/kalendarz/ical" id="subscribeApple" style="display:none; align-items:center; gap:10px; padding:12px 16px; background:#eff6ff; border:1px solid #bfdbfe; border-radius:var(--radius); text-decoration:none; color:#1e40af; font-size:15px; font-weight:600;">
<span style="font-size:22px;">📅</span>
<span>Subskrybuj kalendarz</span>
</a>
<div id="subscribeAndroid" style="display:none;">
<p style="font-size:14px; color:#1f2937; line-height:1.6; margin:0 0 14px 0;">
Google Calendar na telefonie nie pozwala subskrybować zewnętrznych kalendarzy.
Subskrypcję możesz dodać z <strong>komputera</strong> — potem automatycznie zsynchronizuje się z telefonem.
</p>
<div style="background:#f9fafb; border:1px solid #e5e7eb; border-radius:var(--radius); padding:14px; font-size:13px; line-height:1.8;">
<strong>Krok 1.</strong> Skopiuj link:
<div style="display:flex; gap:8px; margin:8px 0 14px 0;">
<input id="icalUrlAndroid" readonly value="https://nordabiznes.pl/kalendarz/ical" style="flex:1; padding:10px 12px; border:1px solid #d1d5db; border-radius:var(--radius); font-size:13px; background:white;">
<button onclick="var i=document.getElementById('icalUrlAndroid');i.select();document.execCommand('copy');this.textContent='Skopiowano!';this.style.background='#059669';var b=this;setTimeout(function(){b.textContent='Kopiuj';b.style.background='#1d4ed8'},2000)" style="padding:10px 16px; background:#1d4ed8; color:white; border:none; border-radius:var(--radius); font-size:13px; cursor:pointer; font-weight:600; white-space:nowrap;">Kopiuj</button>
</div>
<strong>Krok 2.</strong> Na komputerze otwórz <strong>calendar.google.com</strong><br>
<strong>Krok 3.</strong> W lewym panelu kliknij <strong>+</strong> obok "Inne kalendarze"<br>
<strong>Krok 4.</strong> Wybierz <strong>"Z adresu URL"</strong><br>
<strong>Krok 5.</strong> Wklej skopiowany link i kliknij <strong>"Dodaj kalendarz"</strong><br><br>
Wydarzenia pojawią się na telefonie automatycznie po synchronizacji.
</div>
</div>
<script>
var isIOS = /iPad|iPhone|iPod|Macintosh/.test(navigator.userAgent);
if (isIOS) {
document.getElementById('subscribeApple').style.display = 'flex';
} else {
document.getElementById('subscribeAndroid').style.display = 'block';
}
</script>
</div>
<div style="border-top:1px solid #e5e7eb; padding-top:12px;">
<p style="color:#94a3b8; font-size:12px; margin:0 0 6px 0;">Lub skopiuj link ręcznie:</p>
<div style="display:flex; gap:8px;">
<input id="icalUrl" readonly value="https://nordabiznes.pl/kalendarz/ical" style="flex:1; padding:8px 12px; border:1px solid #d1d5db; border-radius:var(--radius); font-size:12px; background:#f9fafb; font-family:monospace;">
<button onclick="copyIcalUrl()" id="copyBtn" style="padding:8px 14px; background:#64748b; color:white; border:none; border-radius:var(--radius); font-size:12px; cursor:pointer; font-weight:500; white-space:nowrap;">Kopiuj</button>
</div>
</div>
</div>
</div>
<!-- Toolbar z przyciskami widoku -->
<div class="calendar-toolbar">
<div class="view-toggle">
<a href="?view=list" class="{% if view_mode == 'list' %}active{% endif %}">
Lista
</a>
<a href="?view=grid&year={{ year }}&month={{ month }}" class="{% if view_mode == 'grid' %}active{% endif %}">
Kalendarz
</a>
</div>
{% if view_mode == 'grid' %}
<div class="month-nav">
<a href="?view=grid&year={{ prev_year }}&month={{ prev_month }}">&larr; Poprzedni</a>
<span class="current-month">{{ month_name }} {{ year }}</span>
<a href="?view=grid&year={{ next_year }}&month={{ next_month }}">Następny &rarr;</a>
</div>
{% endif %}
<label class="filter-toggle" id="external-filter-label" style="display: none;">
<input type="checkbox" id="show-external" checked>
<span id="external-filter-text">Pokaż zewnętrzne</span>
</label>
{% if current_user.can_access_admin_panel() %}
<a href="{{ url_for('admin.admin_calendar') }}" class="btn btn-secondary btn-sm">Zarządzaj</a>
{% endif %}
</div>
{% if view_mode == 'grid' %}
<!-- ================ WIDOK SIATKI ================ -->
<div class="calendar-grid">
<!-- Nagłówki dni tygodnia -->
<div class="calendar-header-row">
<div class="day-header">Pon</div>
<div class="day-header">Wt</div>
<div class="day-header">Sr</div>
<div class="day-header">Czw</div>
<div class="day-header">Pt</div>
<div class="day-header weekend">Sob</div>
<div class="day-header weekend">Nd</div>
</div>
<!-- Tygodnie -->
{% for week in month_days %}
<div class="calendar-week">
{% for day in week %}
{% set is_weekend = loop.index > 5 %}
{% set day_events = events_by_day.get(day, []) if day != 0 else [] %}
<div class="calendar-day {% if day == 0 %}empty{% endif %}{% if day == today.day and month == today.month and year == today.year %} today{% endif %}{% if is_weekend and day != 0 %} weekend{% endif %}{% if day_events %} has-events has-{{ day_events|length }}-event{% endif %}"
{% if day_events %}onclick="openDayModal({{ day }}, {{ month }}, {{ year }})"{% endif %}
data-day="{{ day }}">
{% if day != 0 %}
<div class="day-number">{{ day }}</div>
{% if day_events %}
<div class="calendar-events-wrap">
{% for event in day_events %}
<div class="calendar-event {{ 'featured' if event.is_featured else ('external' if event.is_external else event.event_type) }}"
data-event-id="{{ event.id }}"
data-event-title="{{ event.title }}"
data-event-time="{% if event.time_start %}{{ event.time_start.strftime('%H:%M') }}{% if event.time_end %}-{{ event.time_end.strftime('%H:%M') }}{% endif %}{% endif %}"
data-event-type="{{ 'Zewnętrzne' if event.is_external else ({'meeting':'Spotkanie','networking':'Networking','webinar':'Webinar','conference':'Konferencja','workshop':'Warsztaty'}.get(event.event_type, event.event_type)) }}"
data-event-location="{{ event.location or '' }}"
data-event-url="{{ url_for('calendar.calendar_event', event_id=event.id) }}">
<span class="calendar-event-text">{% if event.time_start %}{{ event.time_start.strftime('%H:%M') }} {% endif %}{{ event.title[:18] }}{% if event.title|length > 18 %}...{% endif %}</span>
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<!-- Day events modal (bottom sheet) -->
<div class="day-modal-overlay" id="dayModalOverlay" onclick="closeDayModal()"></div>
<div class="day-modal" id="dayModal">
<div class="day-modal-handle"></div>
<div class="day-modal-title" id="dayModalTitle"></div>
<div id="dayModalEvents"></div>
</div>
<!-- Legenda typów wydarzeń -->
<div style="margin-top: var(--spacing-lg); display: flex; gap: var(--spacing-lg); flex-wrap: wrap; font-size: var(--font-size-sm); color: var(--text-secondary);">
<span><span class="badge-type" style="background:#fee2e2;color:#991b1b;">Ważne</span></span>
<span><span class="badge-type meeting">Spotkanie</span></span>
<span><span class="badge-type networking">Networking</span></span>
<span><span class="badge-type webinar">Webinar</span></span>
<span><span class="badge-type other">Inne</span></span>
<span><span class="badge-type external">Zewnętrzne</span></span>
</div>
{% else %}
<!-- ================ WIDOK LISTY ================ -->
<!-- Nadchodzące wydarzenia -->
<div class="events-section">
<h2>Nadchodzące wydarzenia</h2>
{% if upcoming_events %}
{% for event in upcoming_events %}
<div class="event-card {% if event.is_featured %}featured{% endif %}{% if event.is_external %} external-event{% endif %}" data-external="{{ 'true' if event.is_external else 'false' }}">
<div class="event-date-box">
<div class="day">{{ event.event_date.day }}</div>
<div class="month">{{ pl_months.get(event.event_date.strftime('%b'), event.event_date.strftime('%b')) }}</div>
</div>
<div class="event-info">
<div class="event-title">
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}">{{ event.title }}</a>{% if event.is_external %} <span style="display:inline-block;background:#94a3b8;color:#fff;font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;vertical-align:middle;">ZEWNĘTRZNE</span>{% elif event.access_level == 'admin_only' %} <span style="display:inline-block;background:#ef4444;color:#fff;font-size:10px;padding:1px 5px;border-radius:3px;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:10px;padding:1px 5px;border-radius:3px;font-weight:600;vertical-align:middle;">IZBA</span>{% endif %}
</div>
{% if event.is_external and event.external_source %}
<div class="external-source">Źródło: {{ event.external_source }}</div>
{% endif %}
<div class="event-meta">
<span class="badge-type {{ 'external' if event.is_external else event.event_type }}">{{ 'Zewnętrzne' if event.is_external else pl_types.get(event.event_type, event.event_type) }}</span>
{% if event.time_start %}
<span>{{ event.time_start.strftime('%H:%M') }}{% if event.time_end %} - {{ event.time_end.strftime('%H:%M') }}{% endif %}</span>
{% endif %}
{% if event.location %}
<span>
{% if event.location_url %}
<a href="{{ event.location_url }}" target="_blank" style="color:var(--primary);text-decoration:none;">{{ event.location }}</a>
{% elif 'do ustalenia' not in event.location|lower and event.location != 'Online' %}
<a href="https://www.google.com/maps/search/{{ event.location|urlencode }}" target="_blank" style="color:var(--primary);text-decoration:none;">{{ event.location }}</a>
{% else %}
{{ event.location }}
{% endif %}
</span>
{% endif %}
{% if event.speaker_name %}
<span>Prelegent: {{ event.speaker_name }}</span>
{% endif %}
</div>
{% if event.description %}
<div class="event-description">{{ event.description|striptags|truncate(150, True) }}</div>
{% endif %}
</div>
<div class="event-actions">
{% if event.is_external %}
<span class="attendee-count">{{ event.attendee_count }} zainteresowanych</span>
{% if event.can_user_attend(current_user) %}
{% set is_attending = event.attendees|selectattr('user_id','equalto',current_user.id)|list %}
<button class="btn btn-sm rsvp-list-btn {{ 'rsvp-attending' if is_attending else 'rsvp-not-attending' }}" data-event-id="{{ event.id }}" data-attending="{{ 'true' if is_attending else 'false' }}" data-external="true" onclick="toggleListRSVP(this)" {{ 'onmouseenter=this.textContent="Nie interesuje" onmouseleave=this.textContent="Zainteresowany "'|safe if is_attending }}>
{{ 'Zainteresowany ✓' if is_attending else 'Zainteresowany' }}
</button>
{% endif %}
{% else %}
<span class="attendee-count">{{ event.attendee_count }} uczestników</span>
{% if event.can_user_attend(current_user) %}
{% set is_attending = event.attendees|selectattr('user_id','equalto',current_user.id)|list %}
<button class="btn btn-sm rsvp-list-btn {{ 'rsvp-attending' if is_attending else 'rsvp-not-attending' }}" data-event-id="{{ event.id }}" data-attending="{{ 'true' if is_attending else 'false' }}" data-external="false" onclick="toggleListRSVP(this)" {{ 'onmouseenter=this.textContent="Wypisz się" onmouseleave=this.textContent="Zapisano "'|safe if is_attending }}>
{{ 'Zapisano ✓' if is_attending else 'Zapisz się' }}
</button>
{% endif %}
{% endif %}
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}" class="btn btn-outline btn-sm">Szczegóły</a>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<p>Brak nadchodzących wydarzeń</p>
</div>
{% endif %}
</div>
<!-- Przeszłe wydarzenia -->
{% if past_events %}
<div class="events-section">
<h2>Ostatnie wydarzenia</h2>
{% for event in past_events %}
<div class="event-card past">
<div class="event-date-box" style="background: var(--secondary);">
<div class="day">{{ event.event_date.day }}</div>
<div class="month">{{ pl_months.get(event.event_date.strftime('%b'), event.event_date.strftime('%b')) }}</div>
</div>
<div class="event-info">
<div class="event-title">
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}">{{ event.title }}</a>
</div>
<div class="event-meta">
<span class="badge-type {{ event.event_type }}">{{ pl_types.get(event.event_type, event.event_type) }}</span>
{% if event.location %}
<span>{{ event.location }}</span>
{% endif %}
</div>
</div>
<div class="event-actions">
<span class="attendee-count">{{ event.attendee_count }} uczestników</span>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}
{% endblock %}
{% block extra_js %}
var csrfToken = '{{ csrf_token() }}';
async function toggleListRSVP(btn) {
var eventId = btn.dataset.eventId;
var isExt = btn.dataset.external === 'true';
btn.disabled = true;
try {
var resp = await fetch('/kalendarz/' + eventId + '/rsvp', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': csrfToken}
});
var data = await resp.json();
if (data.success) {
var addedText = isExt ? 'Zainteresowany ✓' : 'Zapisano ✓';
var removedText = isExt ? 'Zainteresowany' : 'Zapisz się';
var hoverText = isExt ? 'Nie interesuje' : 'Wypisz się';
var countLabel = isExt ? ' zainteresowanych' : ' uczestników';
if (data.action === 'added') {
btn.textContent = addedText;
btn.classList.remove('rsvp-not-attending');
btn.classList.add('rsvp-attending');
btn.dataset.attending = 'true';
btn.onmouseenter = function() { this.textContent = hoverText; };
btn.onmouseleave = function() { this.textContent = addedText; };
} else {
btn.textContent = removedText;
btn.classList.remove('rsvp-attending');
btn.classList.add('rsvp-not-attending');
btn.dataset.attending = 'false';
btn.onmouseenter = null;
btn.onmouseleave = null;
}
var countEl = btn.parentElement.querySelector('.attendee-count');
if (countEl) countEl.textContent = data.attendee_count + countLabel;
}
} catch(e) {}
btn.disabled = false;
}
/* Filter toggle for external events */
(function() {
var cb = document.getElementById('show-external');
var label = document.getElementById('external-filter-label');
var textEl = document.getElementById('external-filter-text');
var stored = localStorage.getItem('nordabiz_show_external');
if (stored === 'false') cb.checked = false;
var extItems = document.querySelectorAll('[data-external="true"]');
var count = extItems.length;
if (count > 0) {
label.style.display = '';
textEl.textContent = 'Pokaż zewnętrzne (' + count + ')';
}
function applyFilter() {
var show = cb.checked;
localStorage.setItem('nordabiz_show_external', show);
extItems.forEach(function(el) {
el.style.display = show ? '' : 'none';
});
}
cb.addEventListener('change', applyFilter);
applyFilter();
})();
var plMonths = ['','stycznia','lutego','marca','kwietnia','maja','czerwca','lipca','sierpnia','września','października','listopada','grudnia'];
function openDayModal(day, month, year) {
var modal = document.getElementById('dayModal');
var overlay = document.getElementById('dayModalOverlay');
var title = document.getElementById('dayModalTitle');
var eventsDiv = document.getElementById('dayModalEvents');
title.textContent = day + ' ' + plMonths[month] + ' ' + year;
// Find events for this day from DOM
var dayCell = document.querySelector('.calendar-day[data-day="' + day + '"].has-events');
if (!dayCell) return;
var events = dayCell.querySelectorAll('.calendar-event');
var html = '';
events.forEach(function(ev) {
var eventType = '';
['meeting','networking','webinar','other','external'].forEach(function(t) {
if (ev.classList.contains(t)) eventType = t;
});
var time = ev.dataset.eventTime || '';
var loc = ev.dataset.eventLocation || '';
var meta = [];
if (ev.dataset.eventType) meta.push(ev.dataset.eventType);
if (time) meta.push(time);
if (loc) meta.push(loc);
html += '<a href="' + ev.dataset.eventUrl + '" class="day-modal-event">' +
'<div class="day-modal-event-dot ' + eventType + '"></div>' +
'<div class="day-modal-event-info">' +
'<div class="day-modal-event-title">' + ev.dataset.eventTitle + '</div>' +
(meta.length ? '<div class="day-modal-event-meta">' + meta.join(' · ') + '</div>' : '') +
'</div>' +
'<span class="day-modal-event-arrow"></span>' +
'</a>';
});
eventsDiv.innerHTML = html;
overlay.classList.add('show');
modal.classList.add('show');
}
function closeDayModal() {
document.getElementById('dayModal').classList.remove('show');
document.getElementById('dayModalOverlay').classList.remove('show');
}
function androidSubscribe() {
// Copy URL to clipboard
var url = 'https://nordabiznes.pl/kalendarz/ical';
if (navigator.clipboard) {
navigator.clipboard.writeText(url);
} else {
var tmp = document.createElement('input');
tmp.value = url;
document.body.appendChild(tmp);
tmp.select();
document.execCommand('copy');
document.body.removeChild(tmp);
}
// Show instructions
document.getElementById('androidStep2').style.display = 'block';
// Open Google Calendar settings (add by URL page)
setTimeout(function() {
window.open('https://calendar.google.com/calendar/r/settings/addbyurl', '_blank');
}, 500);
}
function showSubscribeModal() {
document.getElementById('subscribeModal').style.display = 'flex';
}
function copyIcalUrl() {
var input = document.getElementById('icalUrl');
input.select();
document.execCommand('copy');
var btn = document.getElementById('copyBtn');
btn.textContent = 'Skopiowano!';
setTimeout(function() { btn.textContent = 'Kopiuj'; }, 2000);
}
document.getElementById('subscribeModal').addEventListener('click', function(e) {
if (e.target === this) this.style.display = 'none';
});
{% endblock %}