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
Instead of direct links on colored cells, tapping a day with events
opens a bottom sheet modal showing all events for that day with:
- Event type color dot, title, time, location
- Tap event row to navigate to details
- Polish month names in modal title ("25 marca 2026")
- Smooth slide-up animation, overlay tap to close
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
915 lines
32 KiB
HTML
Executable File
915 lines
32 KiB
HTML
Executable File
{% 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: var(--primary);
|
||
border-width: 2px;
|
||
}
|
||
|
||
.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.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: #f1f5f9;
|
||
color: #475569;
|
||
border-left: 2px solid #94a3b8;
|
||
}
|
||
|
||
/* 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 var(--primary);
|
||
}
|
||
|
||
.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: #f1f5f9; color: #475569; }
|
||
|
||
.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 {
|
||
font-size: 0;
|
||
padding: 0;
|
||
margin: 0;
|
||
white-space: normal;
|
||
border-radius: 0;
|
||
width: 100%;
|
||
flex: 1;
|
||
display: block;
|
||
border-left: none;
|
||
min-height: 12px;
|
||
}
|
||
|
||
/* 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.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: #94a3b8; }
|
||
.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;">×</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;">
|
||
Skopiuj poniższy link i dodaj go jako subskrypcję w swoim kalendarzu.
|
||
Wydarzenia Izby będą się automatycznie synchronizować.
|
||
</p>
|
||
<div style="display:flex; gap:8px; margin-bottom:16px;">
|
||
<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:13px; background:#f9fafb; font-family:monospace;">
|
||
<button onclick="copyIcalUrl()" id="copyBtn" style="padding:8px 16px; background:#1d4ed8; color:white; border:none; border-radius:var(--radius); font-size:13px; cursor:pointer; font-weight:500; white-space:nowrap;">Kopiuj</button>
|
||
</div>
|
||
<div style="font-size:12px; color:#64748b; line-height:1.6;">
|
||
<strong>Jak dodać:</strong><br>
|
||
<strong>iPhone/Mac:</strong> Ustawienia → Konta → Dodaj konto → Inne → Subskrypcja kalendarza → wklej link<br>
|
||
<strong>Google Calendar:</strong> Inne kalendarze (+) → Z adresu URL → wklej link<br>
|
||
<strong>Outlook:</strong> Dodaj kalendarz → Subskrybuj z internetu → wklej link
|
||
</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 }}">← Poprzedni</a>
|
||
<span class="current-month">{{ month_name }} {{ year }}</span>
|
||
<a href="?view=grid&year={{ next_year }}&month={{ next_month }}">Następny →</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 {{ '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) }}">
|
||
</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 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 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 %}
|