nordabiz/templates/calendar/index.html
Maciej Pienczyn eaac876ec2
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
feat(calendar): multi-day events + **bold** w opisach wydarzeń
- norda_events: kolumna event_date_end (NULLABLE, check constraint >= event_date)
- NordaEvent: property is_multi_day, date_range_display; is_past uwzględnia koniec
- Admin (new/edit): pole "Data zakończenia" w formularzu
- Calendar grid: wydarzenie wielodniowe wyświetla się na każdym dniu zakresu
- Upcoming/past filter: używa COALESCE(end, date) — 2-dniowe zostaje w Upcoming
  do swojego ostatniego dnia
- event.html: "Termin" + zakres dla wielodniowych; ICS/Google end date z dateEnd
- Lekki markdown dla opisów: tylko **bold** → <strong> (audyt: tylko event #60)

Zero wpływu na 42 istniejące wydarzenia (NULL == stare zachowanie).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:52:31 +02:00

1205 lines
43 KiB
HTML
Executable File
Raw Permalink 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);
}
}
/* ===== WIDOK KART ===== */
.cards-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-md);
}
.cards-filter {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.cards-filter-label {
font-size: var(--font-size-sm);
color: var(--text-muted);
font-weight: 500;
}
.cards-filter-btn {
padding: var(--spacing-xs) var(--spacing-md);
border-radius: var(--radius-btn);
border: 1.5px solid #cbd5e1;
background: white;
color: var(--text-secondary);
font-size: var(--font-size-sm);
font-weight: 500;
cursor: pointer;
transition: var(--transition);
}
.cards-filter-btn:hover {
border-color: var(--primary);
color: var(--primary);
}
.cards-filter-btn.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.card-banner.hidden-by-filter {
display: none;
}
.card-banner {
background: linear-gradient(135deg, #1e3050 0%, #2E4872 100%);
border-radius: var(--radius-lg);
padding: var(--spacing-md);
color: white;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
box-shadow: var(--shadow-md);
position: relative;
overflow: hidden;
text-decoration: none;
transition: var(--transition);
}
.card-banner:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(46, 72, 114, 0.25);
filter: brightness(1.05);
}
.card-banner::before {
content: '';
position: absolute;
top: -50%;
right: -10%;
width: 200px;
height: 200px;
background: rgba(255,255,255,0.06);
border-radius: 50%;
}
.card-banner.external {
background: linear-gradient(135deg, #1a3a2a 0%, #2d5a3e 100%);
}
.card-banner.external:hover {
box-shadow: 0 10px 30px rgba(45, 90, 62, 0.25);
}
.card-banner.external .btn-card {
color: #2d5a3e;
}
.card-banner-title {
font-size: var(--font-size-base);
font-weight: 700;
line-height: 1.3;
}
.card-banner-meta {
font-size: var(--font-size-xs);
opacity: 0.9;
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
}
.card-banner-bottom {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-sm);
margin-top: auto;
}
.card-banner-attendees {
display: flex;
align-items: center;
gap: var(--spacing-xs);
background: rgba(255,255,255,0.15);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius);
font-weight: 600;
font-size: var(--font-size-xs);
}
.card-banner .btn-card {
background: white;
color: #2E4872;
border: none;
padding: var(--spacing-xs) var(--spacing-md);
font-weight: 600;
font-size: var(--font-size-sm);
border-radius: var(--radius-btn);
text-decoration: none;
cursor: pointer;
transition: var(--transition);
}
.card-banner .btn-card:hover {
background: #EDF0F5;
}
.card-banner .btn-card-registered {
background: #166534;
color: white;
}
@media (max-width: 768px) {
.cards-grid {
grid-template-columns: 1fr;
}
}
</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=cards" class="{% if view_mode == 'cards' %}active{% endif %}">
Karty
</a>
<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>
{% elif view_mode == 'cards' %}
<!-- ================ WIDOK KART ================ -->
<div class="events-section">
<h2>Nadchodzące wydarzenia</h2>
{% if upcoming_events %}
<div class="cards-filter">
<span class="cards-filter-label">Pokaż:</span>
<button class="cards-filter-btn active" onclick="filterCards('all', this)">Wszystkie</button>
<button class="cards-filter-btn" onclick="filterCards('norda', this)">🏢 Norda Biznes</button>
<button class="cards-filter-btn" onclick="filterCards('external', this)">🌐 Zewnętrzne</button>
</div>
<div class="cards-grid" id="cardsGrid">
{% for event in upcoming_events[:12] %}
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}" class="card-banner{% if event.is_external %} external{% endif %}" data-event-type="{{ 'external' if event.is_external else 'norda' }}">
<div class="card-banner-title">
{{ event.title }} →
{% if event.is_external and event.external_source %}
<span style="display:inline-block; background:rgba(255,255,255,0.2); color:#fff; font-size:10px; padding:2px 6px; border-radius:4px; font-weight:600; vertical-align:middle; margin-left:4px;">🌐 {{ event.external_source }}</span>
{% endif %}
</div>
<div class="card-banner-meta">
<span>📆 {{ event.date_range_display }}{% if not event.is_multi_day %} ({{ ['Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob', 'Nd'][event.event_date.weekday()] }}){% endif %}</span>
{% if event.time_start %}
<span>🕕 {{ event.time_start.strftime('%H:%M') }}</span>
{% endif %}
{% if event.location %}
<span>📍 {{ event.location[:30] }}{% if event.location|length > 30 %}...{% endif %}</span>
{% endif %}
</div>
<div class="card-banner-bottom">
<div class="card-banner-attendees">
👥 Zapisanych: {{ event.total_attendee_count }} {% if event.total_attendee_count == 1 %}osoba{% elif event.total_attendee_count in [2,3,4] %}osoby{% else %}osób{% endif %}
</div>
<div>
{% set is_attending = event.attendees|selectattr('user_id','equalto',current_user.id)|list %}
{% if is_attending %}
<span class="btn-card btn-card-registered">✓ Zapisany/a</span>
{% else %}
<span class="btn-card">Zapisz się →</span>
{% endif %}
</div>
</div>
</a>
{% endfor %}
</div>
{% if upcoming_events|length > 12 %}
<div style="margin-top: var(--spacing-lg);">
<a href="?view=list" class="btn btn-outline">Zobacz wszystkie ({{ upcoming_events|length }}) →</a>
</div>
{% endif %}
{% else %}
<div class="empty-state">
<p>Brak nadchodzących wydarzeń</p>
</div>
{% endif %}
</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.total_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.total_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.total_attendee_count }} uczestników</span>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}
{% endblock %}
{% block extra_js %}
var csrfToken = '{{ csrf_token() }}';
function filterCards(type, btn) {
document.querySelectorAll('.cards-filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('#cardsGrid .card-banner').forEach(card => {
if (type === 'all' || card.dataset.eventType === type) {
card.classList.remove('hidden-by-filter');
} else {
card.classList.add('hidden-by-filter');
}
});
}
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 %}