nordabiz/templates/index.html
Maciej Pienczyn 17d89e6296 feat: Banner NordaGPT na stronie głównej prowadzi do /chat
Zamiast otwierać modal, kliknięcie w banner NordaGPT
przenosi bezpośrednio do strony /chat z historią rozmów.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 07:49:31 +01:00

1290 lines
40 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 %}Katalog firm - Norda Biznes Hub{% endblock %}
{% block extra_css %}
/* Event Banner - Ankieta "Kto weźmie udział?" */
.event-banner {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
color: white;
display: flex;
align-items: center;
gap: var(--spacing-lg);
box-shadow: var(--shadow-md);
position: relative;
overflow: hidden;
text-decoration: none;
cursor: pointer;
transition: var(--transition);
}
.event-banner:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
filter: brightness(1.05);
}
.event-banner::before {
content: '';
position: absolute;
top: -50%;
right: -10%;
width: 200px;
height: 200px;
background: rgba(255,255,255,0.1);
border-radius: 50%;
}
.event-banner-icon {
font-size: 2.5rem;
flex-shrink: 0;
}
.event-banner-content {
flex: 1;
min-width: 0;
}
.event-banner-label {
font-size: var(--font-size-xs);
text-transform: uppercase;
letter-spacing: 1px;
opacity: 0.9;
margin-bottom: var(--spacing-xs);
}
.event-banner-title {
font-size: var(--font-size-lg);
font-weight: 700;
margin-bottom: var(--spacing-xs);
}
.event-banner-meta {
font-size: var(--font-size-sm);
opacity: 0.9;
display: flex;
flex-wrap: wrap;
gap: var(--spacing-md);
}
.event-banner-attendees {
display: flex;
align-items: center;
gap: var(--spacing-xs);
background: rgba(255,255,255,0.2);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius);
font-weight: 600;
margin-top: var(--spacing-sm);
}
.event-banner-action {
flex-shrink: 0;
}
.event-banner .btn-light {
background: white;
color: #d97706;
border: none;
padding: var(--spacing-sm) var(--spacing-lg);
font-weight: 600;
font-size: var(--font-size-base);
border-radius: var(--radius);
text-decoration: none;
display: inline-block;
cursor: pointer;
transition: var(--transition);
}
.event-banner .btn-light:disabled {
opacity: 0.7;
cursor: wait;
}
.event-banner .btn-light:hover {
background: #fef3c7;
transform: translateY(-1px);
}
.event-banner .btn-registered {
background: #166534;
color: white;
}
.event-banner .btn-registered:hover {
background: #15803d;
}
@media (max-width: 640px) {
.event-banner {
flex-direction: column;
text-align: center;
}
.event-banner-meta {
justify-content: center;
}
.event-banner-attendees {
justify-content: center;
}
}
/* NordaGPT Chat Banner */
.chat-banner {
background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
color: white;
display: flex;
align-items: center;
gap: var(--spacing-lg);
box-shadow: var(--shadow-md);
position: relative;
overflow: hidden;
transition: var(--transition);
cursor: pointer;
}
.chat-banner:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
filter: brightness(1.05);
}
/* Chat minimized state - banner pulsing to indicate active session */
.chat-banner.chat-active {
animation: chatPulse 2s ease-in-out infinite;
border: 2px solid rgba(255,255,255,0.5);
}
.chat-banner.chat-active .chat-banner-btn {
background: #10b981;
color: white;
}
@keyframes chatPulse {
0%, 100% { box-shadow: var(--shadow-md), 0 0 0 0 rgba(124, 58, 237, 0.4); }
50% { box-shadow: var(--shadow-lg), 0 0 0 8px rgba(124, 58, 237, 0); }
}
.chat-banner::before {
content: '';
position: absolute;
top: -50%;
right: -10%;
width: 200px;
height: 200px;
background: rgba(255,255,255,0.1);
border-radius: 50%;
}
.chat-banner-icon {
font-size: 2.5rem;
flex-shrink: 0;
}
.chat-banner-content {
flex: 1;
min-width: 0;
}
.chat-banner-label {
font-size: var(--font-size-xs);
text-transform: uppercase;
letter-spacing: 1px;
opacity: 0.9;
margin-bottom: var(--spacing-xs);
}
.chat-banner-title {
font-size: var(--font-size-lg);
font-weight: 700;
margin-bottom: var(--spacing-sm);
}
.chat-banner-input-wrapper {
display: flex;
gap: var(--spacing-sm);
align-items: center;
}
.chat-banner-input {
flex: 1;
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-radius: var(--radius);
font-size: var(--font-size-sm);
background: rgba(255,255,255,0.95);
color: var(--text-primary);
cursor: pointer;
}
.chat-banner-input:focus {
outline: none;
background: white;
}
.chat-banner-input::placeholder {
color: var(--text-secondary);
}
.chat-banner-btn {
background: white;
color: #7c3aed;
border: none;
padding: var(--spacing-sm) var(--spacing-md);
font-weight: 600;
font-size: var(--font-size-sm);
border-radius: var(--radius);
cursor: pointer;
transition: var(--transition);
white-space: nowrap;
}
.chat-banner-btn:hover {
background: #f3e8ff;
transform: translateY(-1px);
}
/* NordaGPT Fullscreen Modal */
.nordagpt-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
animation: fadeIn 0.2s ease;
}
.nordagpt-modal.active {
display: flex;
}
.nordagpt-modal.minimized {
display: none;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.nordagpt-container {
position: absolute;
top: 20px;
left: 20px;
right: 20px;
bottom: 20px;
background: white;
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl);
display: flex;
flex-direction: column;
overflow: hidden;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.nordagpt-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-md) var(--spacing-lg);
background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%);
color: white;
}
.nordagpt-header-left {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.nordagpt-header h2 {
font-size: var(--font-size-lg);
font-weight: 600;
margin: 0;
}
.nordagpt-header-badge {
background: rgba(255,255,255,0.2);
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
}
.nordagpt-header-actions {
display: flex;
gap: var(--spacing-xs);
}
.nordagpt-header-btn {
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 32px;
height: 32px;
border-radius: var(--radius);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
}
.nordagpt-header-btn:hover {
background: rgba(255,255,255,0.3);
}
.nordagpt-messages {
flex: 1;
overflow-y: auto;
padding: var(--spacing-lg);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.nordagpt-message {
display: flex;
gap: var(--spacing-sm);
max-width: 85%;
}
.nordagpt-message.user {
align-self: flex-end;
flex-direction: row-reverse;
}
.nordagpt-message-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: var(--font-size-sm);
flex-shrink: 0;
}
.nordagpt-message.assistant .nordagpt-message-avatar {
background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%);
color: white;
}
.nordagpt-message.user .nordagpt-message-avatar {
background: var(--primary);
color: white;
}
.nordagpt-message-content {
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-lg);
line-height: 1.5;
}
.nordagpt-message.assistant .nordagpt-message-content {
background: var(--background);
color: var(--text-primary);
border-bottom-left-radius: var(--radius-sm);
}
.nordagpt-message.user .nordagpt-message-content {
background: var(--primary);
color: white;
border-bottom-right-radius: var(--radius-sm);
}
.nordagpt-message-content a {
color: inherit;
text-decoration: underline;
}
/* AI response list styles */
.nordagpt-message-content .ai-list {
margin: var(--spacing-xs) 0;
padding-left: var(--spacing-lg);
}
.nordagpt-message-content .ai-list li {
margin-bottom: var(--spacing-xs);
line-height: 1.4;
}
.nordagpt-message-content ol.ai-list {
list-style-type: decimal;
}
.nordagpt-message-content ul.ai-list {
list-style-type: disc;
}
.nordagpt-message-content strong {
font-weight: 600;
}
.nordagpt-input-area {
padding: var(--spacing-md) var(--spacing-lg);
border-top: 1px solid var(--border);
display: flex;
gap: var(--spacing-sm);
}
.nordagpt-input {
flex: 1;
padding: var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
font-size: var(--font-size-base);
resize: none;
}
.nordagpt-input:focus {
outline: none;
border-color: #7c3aed;
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
}
.nordagpt-send-btn {
background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%);
color: white;
border: none;
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--radius-lg);
font-weight: 600;
cursor: pointer;
transition: var(--transition);
}
.nordagpt-send-btn:hover {
filter: brightness(1.1);
}
.nordagpt-send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.nordagpt-typing {
display: flex;
gap: 4px;
padding: var(--spacing-sm);
}
.nordagpt-typing span {
width: 8px;
height: 8px;
background: #7c3aed;
border-radius: 50%;
animation: typing 1.4s infinite;
}
.nordagpt-typing span:nth-child(2) { animation-delay: 0.2s; }
.nordagpt-typing span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
30% { transform: translateY(-4px); opacity: 1; }
}
@media (max-width: 640px) {
.chat-banner {
flex-direction: column;
text-align: center;
}
.chat-banner-input-wrapper {
width: 100%;
flex-direction: column;
}
.chat-banner-input {
width: 100%;
}
.nordagpt-container {
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 0;
}
.nordagpt-message {
max-width: 95%;
}
}
/* Search Bar */
.search-section {
margin-bottom: var(--spacing-2xl);
}
.search-bar {
display: flex;
gap: var(--spacing-md);
max-width: 800px;
margin: 0 auto;
}
.search-input {
flex: 1;
padding: var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: var(--font-size-base);
}
.search-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
/* Category Filter */
.category-filter {
display: flex;
gap: var(--spacing-sm);
flex-wrap: wrap;
justify-content: center;
margin-bottom: var(--spacing-xl);
}
.category-badge {
padding: var(--spacing-sm) var(--spacing-md);
background-color: var(--background);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
color: var(--text-secondary);
text-decoration: none;
font-size: var(--font-size-sm);
font-weight: 500;
transition: var(--transition);
cursor: pointer;
}
.category-badge:hover,
.category-badge.active {
background-color: var(--primary);
color: white;
border-color: var(--primary);
}
/* Company Grid */
.companies-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-2xl);
}
.company-card {
background-color: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow);
transition: var(--transition);
display: flex;
flex-direction: column;
height: 100%;
}
.company-card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
.company-logo {
width: 100%;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: var(--spacing-md);
background: var(--background);
border-radius: var(--radius-md);
overflow: hidden;
}
.company-logo img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.company-header {
margin-bottom: var(--spacing-md);
}
.company-category {
display: inline-block;
padding: var(--spacing-xs) var(--spacing-sm);
background-color: var(--background);
color: var(--text-secondary);
font-size: var(--font-size-sm);
border-radius: var(--radius);
margin-bottom: var(--spacing-sm);
}
.company-name {
font-size: var(--font-size-xl);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
text-decoration: none;
display: block;
}
.company-name:hover {
color: var(--primary);
}
.company-description {
color: var(--text-secondary);
font-size: var(--font-size-sm);
line-height: 1.6;
margin-bottom: var(--spacing-md);
flex: 1;
}
.company-contact {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
font-size: var(--font-size-sm);
color: var(--text-secondary);
padding-top: var(--spacing-md);
border-top: 1px solid var(--border);
}
.company-contact-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.company-contact a {
color: var(--primary);
text-decoration: none;
}
.company-contact a:hover {
text-decoration: underline;
}
.empty-state {
text-align: center;
padding: var(--spacing-2xl);
color: var(--text-secondary);
}
.empty-state svg {
opacity: 0.3;
margin-bottom: var(--spacing-md);
}
/* Loading State */
.loading {
text-align: center;
padding: var(--spacing-2xl);
color: var(--text-secondary);
}
@media (max-width: 768px) {
.companies-grid {
grid-template-columns: 1fr;
}
.search-bar {
flex-direction: column;
}
}
{% endblock %}
{% block content %}
<!-- Header -->
<div style="background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); color: white; padding: var(--spacing-xl); border-radius: var(--radius-lg); margin-bottom: var(--spacing-xl); text-align: center;">
<h1 style="font-size: var(--font-size-3xl); margin-bottom: var(--spacing-sm); font-weight: 700;">
Katalog firm Norda Biznes
</h1>
<p style="font-size: var(--font-size-lg); opacity: 0.9;">
{{ total_companies }} firm członkowskich • {{ total_categories }} branż
</p>
</div>
<!-- Event Banner - Ankieta "Kto weźmie udział?" -->
{% if next_event %}
<a href="{{ url_for('calendar_event', event_id=next_event.id) }}" class="event-banner">
<div class="event-banner-icon">📅</div>
<div class="event-banner-content">
<div class="event-banner-label">Najbliższe wydarzenie Kto weźmie udział?</div>
<div class="event-banner-title">{{ next_event.title }} →</div>
<div class="event-banner-meta">
<span>📆 {{ next_event.event_date.strftime('%d.%m.%Y') }} ({{ ['Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob', 'Nd'][next_event.event_date.weekday()] }})</span>
{% if next_event.time_start %}
<span>🕕 {{ next_event.time_start.strftime('%H:%M') }}</span>
{% endif %}
{% if next_event.location %}
<span>📍 {{ next_event.location[:30] }}{% if next_event.location|length > 30 %}...{% endif %}</span>
{% endif %}
</div>
<div class="event-banner-attendees">
👥 Zapisanych: {{ next_event.attendee_count }} {% if next_event.attendee_count == 1 %}osoba{% elif next_event.attendee_count in [2,3,4] %}osoby{% else %}osób{% endif %}
</div>
</div>
<div class="event-banner-action">
{% if user_registered %}
<span class="btn-light btn-registered">✓ Jesteś zapisany/a</span>
{% else %}
<button type="button" class="btn-light" onclick="rsvpAndGo(event, {{ next_event.id }})">Zapisz się →</button>
{% endif %}
</div>
</a>
{% endif %}
<!-- NordaGPT Chat Banner -->
{% if current_user.is_authenticated %}
<a href="{{ url_for('chat') }}" class="chat-banner" id="chatBanner" style="cursor: pointer; text-decoration: none;">
<div class="chat-banner-icon">🤖</div>
<div class="chat-banner-content">
<div class="chat-banner-label">NordaGPT - Asystent AI Norda Biznes</div>
<div class="chat-banner-title" id="chatBannerTitle">Zapytaj o firmy, usługi, wydarzenia...</div>
<div class="chat-banner-input-wrapper">
<span class="chat-banner-input">Np. Kto oferuje usługi IT? Kiedy następne spotkanie?</span>
<span class="chat-banner-btn">Rozpocznij chat →</span>
</div>
</div>
</a>
{% endif %}
<!-- NordaGPT Fullscreen Modal -->
<div class="nordagpt-modal" id="nordagptModal">
<div class="nordagpt-container">
<div class="nordagpt-header">
<div class="nordagpt-header-left">
<span style="font-size: 1.5rem;">🤖</span>
<h2>NordaGPT</h2>
<span class="nordagpt-header-badge">Gemini 2.0</span>
</div>
<div class="nordagpt-header-actions">
<button class="nordagpt-header-btn" onclick="minimizeNordaGPT()" title="Minimalizuj">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 8h8"/>
</svg>
</button>
<button class="nordagpt-header-btn" onclick="closeNordaGPT()" title="Zamknij">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 4l8 8M12 4l-8 8"/>
</svg>
</button>
</div>
</div>
<div class="nordagpt-messages" id="nordagptMessages">
<div class="nordagpt-message assistant">
<div class="nordagpt-message-avatar">AI</div>
<div class="nordagpt-message-content">
Cześć! Jestem NordaGPT - asystentem AI Norda Biznes. Mogę pomóc Ci znaleźć firmy, usługi, sprawdzić kalendarz wydarzeń, rekomendacje i wiele więcej. O co chcesz zapytać?
</div>
</div>
</div>
<div class="nordagpt-input-area">
<input type="text" class="nordagpt-input" id="nordagptInput"
placeholder="Napisz wiadomość..."
onkeypress="if(event.key==='Enter')sendNordaGPTMessage()">
<button class="nordagpt-send-btn" id="nordagptSendBtn" onclick="sendNordaGPTMessage()">
Wyślij
</button>
</div>
</div>
</div>
<!-- Search Section -->
<div class="search-section">
<form action="{{ url_for('search') }}" method="GET" class="search-bar">
<input
type="search"
name="q"
class="search-input"
placeholder="Szukaj firm po nazwie, usłudze lub słowie kluczowym..."
aria-label="Search companies"
>
<button type="submit" class="btn btn-primary">
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="9" cy="9" r="7"/>
<path d="M14 14l5 5"/>
</svg>
Szukaj
</button>
</form>
</div>
<!-- Category Filter -->
{% if categories %}
<div class="category-filter">
<button class="category-badge active" onclick="filterCategory('all')">
Wszystkie ({{ total_companies }})
</button>
{% for category in categories %}
{% set count = companies|selectattr('category_id', 'equalto', category.id)|list|length %}
{% if count > 0 %}
<button class="category-badge" onclick="filterCategory('{{ category.slug }}')">
{{ category.name }} ({{ count }})
</button>
{% endif %}
{% endfor %}
</div>
{% endif %}
<!-- Companies Grid -->
{% if companies %}
<div class="companies-grid" id="companiesGrid">
{% for company in companies %}
<div class="company-card" data-category="{{ company.category.slug if company.category else 'brak' }}">
<a href="{{ url_for('company_detail', company_id=company.id) }}" class="company-logo">
<img src="{{ url_for('static', filename='img/companies/' ~ company.slug ~ '.webp') }}"
alt="{{ company.name }}"
onerror="if(!this.dataset.triedSvg){this.dataset.triedSvg='1';this.src=this.src.replace('.webp','.svg')}else{this.parentElement.style.display='none'}">
</a>
<div class="company-header">
{% if company.category %}
<span class="company-category">{{ company.category.name }}</span>
{% endif %}
<a href="{{ url_for('company_detail', company_id=company.id) }}" class="company-name">
{{ company.name }}
</a>
</div>
<div class="company-description">
{{ company.description_short|truncate(150) if company.description_short else 'Brak opisu' }}
</div>
<div class="company-contact">
{% if company.website %}
<div class="company-contact-item">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="8" cy="8" r="7"/>
<path d="M1 8h14M8 1a11 11 0 0 1 0 14 11 11 0 0 1 0-14"/>
</svg>
<a href="{{ company.website }}" target="_blank" rel="noopener noreferrer">
{{ company.website|replace('https://', '')|replace('http://', '')|replace('www.', '')|truncate(30) }}
</a>
</div>
{% endif %}
{% if company.phone %}
<div class="company-contact-item">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/>
<path d="M6 6l4 4M10 6l-4 4"/>
</svg>
<a href="tel:{{ company.phone }}">{{ company.phone }}</a>
</div>
{% endif %}
{% if company.address_city %}
<div class="company-contact-item">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
<path d="M8 2l6 6-6 6-6-6 6-6z"/>
</svg>
{{ company.address_city }}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
<circle cx="60" cy="60" r="50" stroke="currentColor" stroke-width="4"/>
<path d="M40 50h40M40 70h40" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
</svg>
<h2>Brak firm w katalogu</h2>
<p>Nie znaleziono żadnych firm w systemie.</p>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
// RSVP and redirect to event
async function rsvpAndGo(e, eventId) {
e.preventDefault();
e.stopPropagation();
const btn = e.target;
btn.disabled = true;
btn.textContent = 'Zapisuję...';
try {
const response = await fetch('/kalendarz/' + eventId + '/rsvp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
}
});
const data = await response.json();
if (data.success) {
// Update counter visually before redirect
const counter = document.querySelector('.event-banner-attendees');
if (counter && data.action === 'added') {
const newCount = data.attendee_count;
let suffix = 'osób';
if (newCount === 1) suffix = 'osoba';
else if (newCount >= 2 && newCount <= 4) suffix = 'osoby';
counter.innerHTML = '👥 Zapisanych: ' + newCount + ' ' + suffix;
}
btn.textContent = '✓ Zapisano!';
// Redirect after short delay
setTimeout(() => {
window.location.href = '/kalendarz/' + eventId;
}, 500);
} else {
btn.textContent = 'Błąd';
setTimeout(() => { btn.textContent = 'Zapisz się →'; btn.disabled = false; }, 2000);
}
} catch (error) {
btn.textContent = 'Błąd sieci';
setTimeout(() => { btn.textContent = 'Zapisz się →'; btn.disabled = false; }, 2000);
}
}
// Category filter
function filterCategory(slug) {
const cards = document.querySelectorAll('.company-card');
const badges = document.querySelectorAll('.category-badge');
// Update active badge
badges.forEach(badge => {
badge.classList.remove('active');
if (badge.textContent.toLowerCase().includes(slug) ||
(slug === 'all' && badge.textContent.includes('Wszystkie'))) {
badge.classList.add('active');
}
});
// Filter cards
cards.forEach(card => {
if (slug === 'all') {
card.style.display = 'flex';
} else {
const cardCategory = card.getAttribute('data-category');
card.style.display = cardCategory === slug ? 'flex' : 'none';
}
});
}
// Smooth scroll to companies on search
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('q')) {
document.getElementById('companiesGrid')?.scrollIntoView({ behavior: 'smooth' });
}
// ========================================
// NordaGPT Chat Functions
// ========================================
let nordaGPTConversationId = null;
let nordaGPTIsMinimized = false;
function openNordaGPT() {
document.getElementById('nordagptModal').classList.add('active');
document.getElementById('nordagptModal').classList.remove('minimized');
document.getElementById('nordagptInput').focus();
document.body.style.overflow = 'hidden';
nordaGPTIsMinimized = false;
// Remove active indicator from banner
const banner = document.getElementById('chatBanner');
if (banner) {
banner.classList.remove('chat-active');
}
}
function minimizeNordaGPT() {
document.getElementById('nordagptModal').classList.remove('active');
document.getElementById('nordagptModal').classList.add('minimized');
document.body.style.overflow = '';
nordaGPTIsMinimized = true;
// Show banner with active indicator
const banner = document.getElementById('chatBanner');
const title = document.getElementById('chatBannerTitle');
const btn = banner?.querySelector('.chat-banner-btn');
if (banner) {
banner.classList.add('chat-active');
}
if (title) {
title.textContent = '💬 Chat aktywny - kliknij aby kontynuować';
}
if (btn) {
btn.textContent = 'Wznów chat →';
}
}
function closeNordaGPT() {
document.getElementById('nordagptModal').classList.remove('active');
document.getElementById('nordagptModal').classList.remove('minimized');
document.body.style.overflow = '';
nordaGPTIsMinimized = false;
// Reset banner to initial state
const banner = document.getElementById('chatBanner');
const title = document.getElementById('chatBannerTitle');
const btn = banner?.querySelector('.chat-banner-btn');
if (banner) {
banner.classList.remove('chat-active');
}
if (title) {
title.textContent = 'Zapytaj o firmy, usługi, wydarzenia...';
}
if (btn) {
btn.textContent = 'Rozpocznij chat →';
}
}
// Convert URLs, emails, markdown to HTML (linkify + formatting)
function linkifyNordaGPT(text) {
let escaped = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
// Use placeholders to protect converted elements
const placeholders = [];
function addPlaceholder(html) {
const placeholder = '__PH_' + placeholders.length + '__';
placeholders.push(html);
return placeholder;
}
// 1. Markdown links first
const markdownLinkRegex = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/gi;
escaped = escaped.replace(markdownLinkRegex, function(match, linkText, url) {
return addPlaceholder('<a href="' + url + '" target="_blank">' + linkText + '</a>');
});
// 2. Plain URLs
const urlRegex = /(https?:\/\/[^\s<]+|www\.[^\s<]+)/gi;
escaped = escaped.replace(urlRegex, function(url) {
let cleanUrl = url.replace(/[.,;:!?)\]]+$/, '');
const trailingPunct = url.slice(cleanUrl.length);
const href = cleanUrl.startsWith('www.') ? 'https://' + cleanUrl : cleanUrl;
return addPlaceholder('<a href="' + href + '" target="_blank">' + cleanUrl + '</a>') + trailingPunct;
});
// 3. Emails
const emailRegex = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/gi;
escaped = escaped.replace(emailRegex, function(email) {
let cleanEmail = email.replace(/[.,;:!?)\]]+$/, '');
const trailingPunct = email.slice(cleanEmail.length);
return addPlaceholder('<a href="mailto:' + cleanEmail + '">' + cleanEmail + '</a>') + trailingPunct;
});
// 4. Convert **bold** to <strong>
escaped = escaped.replace(/\*\*([^*]+)\*\*/g, function(match, boldText) {
return addPlaceholder('<strong>' + boldText + '</strong>');
});
// 5. Process lines for lists and newlines
const lines = escaped.split('\n');
const processedLines = [];
let inList = false;
let listType = null;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
const trimmedLine = line.trim();
// Check for numbered list (1. 2. 3. etc.)
const numberedMatch = trimmedLine.match(/^(\d+)\.\s+(.*)$/);
// Check for bullet list (- or * at start)
const bulletMatch = trimmedLine.match(/^[-*]\s+(.*)$/);
if (numberedMatch) {
if (!inList || listType !== 'ol') {
if (inList) processedLines.push(listType === 'ol' ? '</ol>' : '</ul>');
processedLines.push('<ol class="ai-list">');
inList = true;
listType = 'ol';
}
processedLines.push('<li>' + numberedMatch[2] + '</li>');
} else if (bulletMatch) {
if (!inList || listType !== 'ul') {
if (inList) processedLines.push(listType === 'ol' ? '</ol>' : '</ul>');
processedLines.push('<ul class="ai-list">');
inList = true;
listType = 'ul';
}
processedLines.push('<li>' + bulletMatch[1] + '</li>');
} else {
if (inList && trimmedLine !== '') {
processedLines.push(listType === 'ol' ? '</ol>' : '</ul>');
inList = false;
listType = null;
}
if (trimmedLine === '') {
if (!inList) processedLines.push('<br>');
} else {
processedLines.push(line);
if (i < lines.length - 1) processedLines.push('<br>');
}
}
}
if (inList) {
processedLines.push(listType === 'ol' ? '</ol>' : '</ul>');
}
escaped = processedLines.join('\n');
// 6. Restore all placeholders
placeholders.forEach(function(html, i) {
escaped = escaped.replace('__PH_' + i + '__', html);
});
// Clean up multiple consecutive <br> tags
escaped = escaped.replace(/(<br>\s*){3,}/g, '<br><br>');
return escaped;
}
function addNordaGPTMessage(role, content) {
const messagesDiv = document.getElementById('nordagptMessages');
const messageDiv = document.createElement('div');
messageDiv.className = 'nordagpt-message ' + role;
const avatar = document.createElement('div');
avatar.className = 'nordagpt-message-avatar';
avatar.textContent = role === 'user' ? 'U' : 'AI';
const contentDiv = document.createElement('div');
contentDiv.className = 'nordagpt-message-content';
if (role === 'assistant') {
contentDiv.innerHTML = linkifyNordaGPT(content);
} else {
contentDiv.textContent = content;
}
messageDiv.appendChild(avatar);
messageDiv.appendChild(contentDiv);
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function showNordaGPTTyping() {
const messagesDiv = document.getElementById('nordagptMessages');
const typingDiv = document.createElement('div');
typingDiv.className = 'nordagpt-message assistant';
typingDiv.id = 'nordagptTyping';
typingDiv.innerHTML = `
<div class="nordagpt-message-avatar">AI</div>
<div class="nordagpt-message-content">
<div class="nordagpt-typing"><span></span><span></span><span></span></div>
</div>
`;
messagesDiv.appendChild(typingDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function hideNordaGPTTyping() {
const typing = document.getElementById('nordagptTyping');
if (typing) typing.remove();
}
async function sendNordaGPTMessage() {
const input = document.getElementById('nordagptInput');
const sendBtn = document.getElementById('nordagptSendBtn');
const message = input.value.trim();
if (!message) return;
// Add user message
addNordaGPTMessage('user', message);
input.value = '';
sendBtn.disabled = true;
// Show typing indicator
showNordaGPTTyping();
try {
// Step 1: Start conversation if we don't have one
if (!nordaGPTConversationId) {
const startResponse = await fetch('/api/chat/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({
title: 'NordaGPT - ' + new Date().toLocaleDateString('pl-PL')
})
});
const startData = await startResponse.json();
if (startData.success) {
nordaGPTConversationId = startData.conversation_id;
} else {
throw new Error(startData.error || 'Nie udało się rozpocząć rozmowy');
}
}
// Step 2: Send message to conversation
const response = await fetch('/api/chat/' + nordaGPTConversationId + '/message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({
message: message
})
});
const data = await response.json();
hideNordaGPTTyping();
if (data.success && data.message) {
addNordaGPTMessage('assistant', data.message);
} else if (data.error) {
addNordaGPTMessage('assistant', 'Przepraszam, wystąpił błąd: ' + data.error);
}
} catch (error) {
hideNordaGPTTyping();
console.error('NordaGPT error:', error);
addNordaGPTMessage('assistant', 'Przepraszam, nie mogę teraz odpowiedzieć. Spróbuj ponownie później.');
}
sendBtn.disabled = false;
input.focus();
}
// Close modal on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const modal = document.getElementById('nordagptModal');
if (modal.classList.contains('active')) {
minimizeNordaGPT();
}
}
});
// Close modal when clicking outside
document.getElementById('nordagptModal')?.addEventListener('click', function(e) {
if (e.target === this) {
minimizeNordaGPT();
}
});
{% endblock %}