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
- Display up to 3 next events with RSVP status instead of just one - Add import script for WhatsApp Norda group data (Feb 2026): events, company updates, Alter Energy, Croatia announcement Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1591 lines
54 KiB
HTML
Executable File
1591 lines
54 KiB
HTML
Executable File
{% extends "base.html" %}
|
||
|
||
{% block title %}Katalog firm - Norda Biznes Partner{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
/* Event Banner - Ankieta "Kto weźmie udział?" (niebieski primary) */
|
||
.event-banner {
|
||
background: linear-gradient(135deg, #1e3050 0%, #2E4872 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: 0 10px 30px rgba(46, 72, 114, 0.25);
|
||
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: #2E4872;
|
||
border: none;
|
||
padding: var(--spacing-sm) var(--spacing-lg);
|
||
font-weight: 600;
|
||
font-size: var(--font-size-base);
|
||
border-radius: var(--radius-btn);
|
||
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: #EDF0F5;
|
||
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 (niebieski primary) */
|
||
.chat-banner {
|
||
background: linear-gradient(135deg, #1e3050 0%, #2E4872 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: 0 10px 30px rgba(46, 72, 114, 0.25);
|
||
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(46, 72, 114, 0.4); }
|
||
50% { box-shadow: var(--shadow-lg), 0 0 0 8px rgba(46, 72, 114, 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: #2E4872;
|
||
border: none;
|
||
padding: var(--spacing-sm) var(--spacing-md);
|
||
font-weight: 600;
|
||
font-size: var(--font-size-sm);
|
||
border-radius: var(--radius-btn);
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.chat-banner-btn:hover {
|
||
background: #EDF0F5;
|
||
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, #1e3050 0%, #2E4872 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, #1e3050 0%, #2E4872 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: #2E4872;
|
||
box-shadow: 0 0 0 3px rgba(46, 72, 114, 0.1);
|
||
}
|
||
|
||
.nordagpt-send-btn {
|
||
background: linear-gradient(135deg, #1e3050 0%, #2E4872 100%);
|
||
color: white;
|
||
border: none;
|
||
padding: var(--spacing-md) var(--spacing-lg);
|
||
border-radius: var(--radius-btn);
|
||
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: #2E4872;
|
||
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);
|
||
}
|
||
|
||
/* Category hierarchy - two rows */
|
||
.category-filter-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--spacing-md);
|
||
margin-bottom: var(--spacing-xl);
|
||
}
|
||
|
||
.category-filter-main {
|
||
display: flex;
|
||
gap: var(--spacing-sm);
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
}
|
||
|
||
.category-filter-sub {
|
||
display: none;
|
||
gap: var(--spacing-sm);
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
padding: var(--spacing-md);
|
||
background: var(--bg-secondary);
|
||
border-radius: var(--radius-lg);
|
||
}
|
||
|
||
.category-filter-sub.visible {
|
||
display: flex;
|
||
}
|
||
|
||
.category-badge.category-main {
|
||
font-weight: 600;
|
||
background-color: var(--surface);
|
||
color: var(--text-primary);
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.category-badge.category-main:hover {
|
||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark, #1d4ed8) 100%);
|
||
color: white;
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.category-badge.category-main.active {
|
||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark, #1d4ed8) 100%);
|
||
color: white;
|
||
border-color: var(--primary);
|
||
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3);
|
||
}
|
||
|
||
.category-badge.category-sub {
|
||
font-size: var(--font-size-sm);
|
||
padding: var(--spacing-xs) var(--spacing-md);
|
||
background-color: var(--surface);
|
||
border-color: var(--border);
|
||
}
|
||
|
||
.category-badge.category-sub:hover,
|
||
.category-badge.category-sub.active {
|
||
background-color: var(--primary);
|
||
color: white;
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
/* Kategoria "Do uzupełnienia" - żółty styl, z prawej strony */
|
||
.category-badge.category-todo {
|
||
margin-left: auto;
|
||
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
|
||
color: #78350f;
|
||
border-color: #f59e0b;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.category-badge.category-todo:hover {
|
||
filter: brightness(1.1);
|
||
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.4);
|
||
}
|
||
|
||
.category-badge.category-todo.active {
|
||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.5);
|
||
}
|
||
|
||
/* 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: 0;
|
||
padding: var(--spacing-lg);
|
||
border: 1px solid #E4E4E4;
|
||
transition: var(--transition);
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
|
||
.company-card:hover {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 10px 30px rgba(46, 72, 114, 0.15);
|
||
}
|
||
|
||
.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: #EDF0F5;
|
||
color: #464646;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
border-radius: 4px;
|
||
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 - różny dla członków i nie-członków -->
|
||
{% if current_user.is_authenticated and not current_user.is_norda_member and not current_user.company_id %}
|
||
{% if pending_application %}
|
||
<!-- Banner dla użytkownika z deklaracją w toku -->
|
||
<a href="{{ url_for('membership.status') }}" class="membership-header" data-animate="fadeIn" style="display: flex; align-items: center; gap: var(--spacing-xl); padding: var(--spacing-xl); background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); border-radius: var(--radius-lg); margin-bottom: var(--spacing-xl); text-decoration: none; color: white;">
|
||
<div style="font-size: 3rem;">📋</div>
|
||
<div style="flex: 1;">
|
||
<h1 style="font-size: var(--font-size-2xl); margin-bottom: var(--spacing-xs); font-weight: 700;">
|
||
Deklaracja członkowska w toku
|
||
</h1>
|
||
<p style="font-size: var(--font-size-md); opacity: 0.9; margin: 0;">
|
||
{% if pending_application.status == 'submitted' or pending_application.status == 'under_review' %}
|
||
Twoja deklaracja dla firmy "{{ pending_application.company_name }}" oczekuje na rozpatrzenie
|
||
{% elif pending_application.status == 'pending_user_approval' %}
|
||
Administrator zaproponował zmiany - sprawdź i zaakceptuj
|
||
{% elif pending_application.status == 'changes_requested' %}
|
||
Wymagane są poprawki w Twojej deklaracji
|
||
{% else %}
|
||
Kontynuuj wypełnianie deklaracji dla firmy "{{ pending_application.company_name or 'Twoja firma' }}"
|
||
{% endif %}
|
||
</p>
|
||
</div>
|
||
<div style="background: white; color: #1d4ed8; padding: var(--spacing-md) var(--spacing-xl); border-radius: var(--radius); font-weight: 700; font-size: var(--font-size-lg); white-space: nowrap;">
|
||
{% if pending_application.status == 'draft' %}Kontynuuj →{% else %}Sprawdź status →{% endif %}
|
||
</div>
|
||
</a>
|
||
<div style="background: #dbeafe; border: 1px solid #93c5fd; padding: var(--spacing-md); border-radius: var(--radius); margin-bottom: var(--spacing-xl); display: flex; align-items: center; gap: var(--spacing-sm);">
|
||
<span style="font-size: 1.2rem;">⏳</span>
|
||
<span style="color: #1e40af;">Przeglądasz listę firm Izby NORDA. Pełny dostęp do szczegółów firm otrzymasz po zatwierdzeniu deklaracji.</span>
|
||
</div>
|
||
{% else %}
|
||
<!-- Banner CTA dla nie-członków NORDA bez deklaracji -->
|
||
<a href="{{ url_for('membership.apply') }}" class="membership-header" data-animate="fadeIn" style="display: flex; align-items: center; gap: var(--spacing-xl); padding: var(--spacing-xl); background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); border-radius: var(--radius-lg); margin-bottom: var(--spacing-xl); text-decoration: none; color: white;">
|
||
<div style="font-size: 3rem;">🤝</div>
|
||
<div style="flex: 1;">
|
||
<h1 style="font-size: var(--font-size-2xl); margin-bottom: var(--spacing-xs); font-weight: 700;">
|
||
Dołącz do Izby Przedsiębiorców NORDA
|
||
</h1>
|
||
<p style="font-size: var(--font-size-md); opacity: 0.9; margin: 0;">
|
||
Złóż deklarację członkowską i zyskaj pełny dostęp do katalog {{ total_companies }} firm, wydarzeń i funkcji portalu
|
||
</p>
|
||
</div>
|
||
<div style="background: white; color: #16a34a; padding: var(--spacing-md) var(--spacing-xl); border-radius: var(--radius); font-weight: 700; font-size: var(--font-size-lg); white-space: nowrap;">
|
||
Złóż deklarację →
|
||
</div>
|
||
</a>
|
||
<div style="background: #fef3c7; border: 1px solid #fde68a; padding: var(--spacing-md); border-radius: var(--radius); margin-bottom: var(--spacing-xl); display: flex; align-items: center; gap: var(--spacing-sm);">
|
||
<span style="font-size: 1.2rem;">🔒</span>
|
||
<span style="color: #92400e;">Przeglądasz listę firm Izby NORDA. Aby zobaczyć szczegóły każdej firmy, złóż deklarację członkowską.</span>
|
||
</div>
|
||
{% endif %}
|
||
{% else %}
|
||
<!-- Standardowy nagłówek dla członków -->
|
||
<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;">
|
||
{{ COMPANY_COUNT }} podmiotów gospodarczych • 4 kategorie • 17 podkategorii
|
||
</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Event Banners - Najbliższe wydarzenia -->
|
||
{% if upcoming_events %}
|
||
{% for ue in upcoming_events %}
|
||
{% set ev = ue.event %}
|
||
<a href="{{ url_for('calendar.calendar_event', event_id=ev.id) }}" class="event-banner" data-animate="fadeIn"{% if not loop.first %} style="margin-top: -8px;"{% endif %}>
|
||
<div class="event-banner-icon">📅</div>
|
||
<div class="event-banner-content">
|
||
{% if loop.first %}
|
||
<div class="event-banner-label">Najbliższe wydarzenia – Kto weźmie udział?</div>
|
||
{% endif %}
|
||
<div class="event-banner-title">{{ ev.title }} →</div>
|
||
<div class="event-banner-meta">
|
||
<span>📆 {{ ev.event_date.strftime('%d.%m.%Y') }} ({{ ['Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob', 'Nd'][ev.event_date.weekday()] }})</span>
|
||
{% if ev.time_start %}
|
||
<span>🕕 {{ ev.time_start.strftime('%H:%M') }}</span>
|
||
{% endif %}
|
||
{% if ev.location %}
|
||
<span>📍 {{ ev.location[:30] }}{% if ev.location|length > 30 %}...{% endif %}</span>
|
||
{% endif %}
|
||
</div>
|
||
<div class="event-banner-attendees">
|
||
👥 Zapisanych: {{ ev.attendee_count }} {% if ev.attendee_count == 1 %}osoba{% elif ev.attendee_count in [2,3,4] %}osoby{% else %}osób{% endif %}
|
||
</div>
|
||
</div>
|
||
<div class="event-banner-action">
|
||
{% if ue.user_registered %}
|
||
<span class="btn-light btn-registered">✓ Jesteś zapisany/a</span>
|
||
{% elif ue.user_can_attend %}
|
||
<button type="button" class="btn-light" onclick="rsvpAndGo(event, {{ ev.id }})">Zapisz się →</button>
|
||
{% elif ev.access_level == 'rada_only' %}
|
||
<span class="btn-light" style="background: #fef3c7; color: #92400e; border: 1px solid #fde68a;">🔒 Rada Izby</span>
|
||
{% endif %}
|
||
</div>
|
||
</a>
|
||
{% endfor %}
|
||
{% 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;" data-animate="fadeIn">
|
||
<div class="chat-banner-icon"><img src="{{ url_for('static', filename='img/nordagpt-icon.svg') }}" alt="NordaGPT" style="width: 48px; height: 48px;"></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">
|
||
<img src="{{ url_for('static', filename='img/nordagpt-icon.svg') }}" alt="NordaGPT" style="width: 28px; height: 28px;">
|
||
<h2>NordaGPT</h2>
|
||
<span class="nordagpt-header-badge">Gemini 3</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>
|
||
|
||
<!-- ZOPK Knowledge Widget — admin only -->
|
||
{% if current_user.is_authenticated and current_user.is_admin and zopk_facts %}
|
||
<div style="background: linear-gradient(135deg, #059669 0%, #047857 50%, #065f46 100%); border-radius: var(--radius-lg); padding: var(--spacing-lg); margin-bottom: var(--spacing-xl); color: white;" data-animate="fadeIn">
|
||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-md);">
|
||
<span style="font-size: 1.5rem;">💡</span>
|
||
<h3 style="margin: 0; font-size: var(--font-size-lg); font-weight: 700;">Czy wiesz, że... <span style="font-weight: 400; font-size: var(--font-size-sm); opacity: 0.8;">(Baza Wiedzy ZOPK)</span></h3>
|
||
</div>
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--spacing-md);">
|
||
{% for fact in zopk_facts %}
|
||
<div style="background: rgba(255,255,255,0.12); border-radius: var(--radius); padding: var(--spacing-md);">
|
||
<span style="display: inline-block; padding: 1px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; margin-bottom: var(--spacing-xs);
|
||
{% if fact.fact_type == 'investment' %}background: rgba(16,185,129,0.3);
|
||
{% elif fact.fact_type == 'event' %}background: rgba(59,130,246,0.3);
|
||
{% elif fact.fact_type == 'decision' %}background: rgba(245,158,11,0.3);
|
||
{% elif fact.fact_type == 'milestone' %}background: rgba(139,92,246,0.3);
|
||
{% else %}background: rgba(255,255,255,0.2);{% endif %}">
|
||
{{ fact.fact_type or 'fakt' }}
|
||
</span>
|
||
<p style="font-size: var(--font-size-sm); line-height: 1.5; margin: 0; opacity: 0.95;">
|
||
{{ fact.full_text[:200] }}{% if fact.full_text|length > 200 %}...{% endif %}
|
||
</p>
|
||
{% if fact.source_news %}
|
||
<div style="font-size: 11px; margin-top: var(--spacing-xs); opacity: 0.7;">
|
||
{{ fact.source_news.source_name or fact.source_news.source_domain }} • {{ fact.source_news.published_at.strftime('%d.%m.%Y') if fact.source_news.published_at else '' }}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Search Section -->
|
||
<div class="search-section" data-animate="fadeIn">
|
||
<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 - Hierarchical -->
|
||
{% if main_categories %}
|
||
<div class="category-filter-wrapper">
|
||
<!-- Row 1: Main categories -->
|
||
<div class="category-filter-main">
|
||
<button class="category-badge active" onclick="filterCategory('all')">
|
||
Wszystkie ({{ total_companies }})
|
||
</button>
|
||
{# Collect categories with counts and sort by count descending #}
|
||
{% set cat_counts = [] %}
|
||
{% for main_cat in main_categories %}
|
||
{% set main_count = companies|selectattr('category_id', 'equalto', main_cat.id)|list|length %}
|
||
{% set sub_count = namespace(val=0) %}
|
||
{% for sub in main_cat.subcategories %}
|
||
{% set sub_count.val = sub_count.val + companies|selectattr('category_id', 'equalto', sub.id)|list|length %}
|
||
{% endfor %}
|
||
{% set total_count = main_count + sub_count.val %}
|
||
{% if total_count > 0 %}
|
||
{% set _ = cat_counts.append({'cat': main_cat, 'count': total_count}) %}
|
||
{% endif %}
|
||
{% endfor %}
|
||
{# Renderuj normalne kategorie (bez "do-uzupelnienia") #}
|
||
{% for item in cat_counts|sort(attribute='count', reverse=true) %}
|
||
{% if item.cat.slug != 'do-uzupelnienia' %}
|
||
<button class="category-badge category-main" onclick="selectMainCategory('{{ item.cat.slug }}')" data-main-slug="{{ item.cat.slug }}">
|
||
{{ item.cat.name }} ({{ item.count }})
|
||
</button>
|
||
{% endif %}
|
||
{% endfor %}
|
||
{# Kategoria "Do uzupełnienia" osobno, z prawej strony (żółta) #}
|
||
{% for item in cat_counts if item.cat.slug == 'do-uzupelnienia' %}
|
||
<button class="category-badge category-todo" onclick="selectMainCategory('{{ item.cat.slug }}')" data-main-slug="{{ item.cat.slug }}">
|
||
{{ item.cat.name }} ({{ item.count }})
|
||
</button>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<!-- Row 2: Subcategories (hidden by default, shown when main category selected) -->
|
||
{% for main_cat in main_categories %}
|
||
<div class="category-filter-sub" id="subcats-{{ main_cat.slug }}" data-parent="{{ main_cat.slug }}">
|
||
{# Zbierz podkategorie z licznikami i posortuj malejąco #}
|
||
{% set sub_counts = [] %}
|
||
{% for sub in main_cat.subcategories %}
|
||
{% set count = companies|selectattr('category_id', 'equalto', sub.id)|list|length %}
|
||
{% if count > 0 %}
|
||
{% set _ = sub_counts.append({'sub': sub, 'count': count}) %}
|
||
{% endif %}
|
||
{% endfor %}
|
||
{% for item in sub_counts|sort(attribute='count', reverse=true) %}
|
||
<button class="category-badge category-sub" onclick="filterSubCategory('{{ item.sub.slug }}', '{{ main_cat.slug }}')" data-parent="{{ main_cat.slug }}">
|
||
{{ item.sub.name }} ({{ item.count }})
|
||
</button>
|
||
{% endfor %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% elif categories %}
|
||
<!-- Fallback dla starej struktury -->
|
||
<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' }}" data-animate="fadeInUp" data-animate-delay="{{ (loop.index0 % 6) + 1 }}">
|
||
<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 - show all
|
||
function filterCategory(slug) {
|
||
const cards = document.querySelectorAll('.company-card');
|
||
|
||
// Hide all subcategory rows
|
||
document.querySelectorAll('.category-filter-sub').forEach(row => {
|
||
row.classList.remove('visible');
|
||
});
|
||
|
||
// Remove active from all badges
|
||
document.querySelectorAll('.category-badge').forEach(badge => {
|
||
badge.classList.remove('active');
|
||
});
|
||
|
||
// Activate "Wszystkie" button
|
||
const allBtn = document.querySelector('.category-badge[onclick*="filterCategory(\'all\')"]');
|
||
if (allBtn) allBtn.classList.add('active');
|
||
|
||
// Show all cards
|
||
cards.forEach(card => {
|
||
card.style.display = 'flex';
|
||
});
|
||
}
|
||
|
||
// Select main category - show subcategories and filter
|
||
function selectMainCategory(mainSlug) {
|
||
const cards = document.querySelectorAll('.company-card');
|
||
|
||
// Hide all subcategory rows, show only selected
|
||
document.querySelectorAll('.category-filter-sub').forEach(row => {
|
||
row.classList.remove('visible');
|
||
});
|
||
const subRow = document.getElementById('subcats-' + mainSlug);
|
||
if (subRow) subRow.classList.add('visible');
|
||
|
||
// Update active badges
|
||
document.querySelectorAll('.category-badge').forEach(badge => {
|
||
badge.classList.remove('active');
|
||
});
|
||
const mainBtn = document.querySelector('.category-badge.category-main[data-main-slug="' + mainSlug + '"]');
|
||
if (mainBtn) mainBtn.classList.add('active');
|
||
|
||
// Get all valid slugs (main + subcategories)
|
||
const validSlugs = [mainSlug];
|
||
document.querySelectorAll('.category-badge.category-sub[data-parent="' + mainSlug + '"]').forEach(badge => {
|
||
const onclick = badge.getAttribute('onclick');
|
||
if (onclick) {
|
||
const match = onclick.match(/filterSubCategory\('([^']+)'/);
|
||
if (match) validSlugs.push(match[1]);
|
||
}
|
||
});
|
||
|
||
// Filter cards
|
||
cards.forEach(card => {
|
||
const cardCategory = card.getAttribute('data-category');
|
||
card.style.display = validSlugs.includes(cardCategory) ? 'flex' : 'none';
|
||
});
|
||
}
|
||
|
||
// Filter by subcategory
|
||
function filterSubCategory(subSlug, parentSlug) {
|
||
const cards = document.querySelectorAll('.company-card');
|
||
|
||
// Keep subcategory row visible
|
||
document.querySelectorAll('.category-filter-sub').forEach(row => {
|
||
row.classList.remove('visible');
|
||
});
|
||
const subRow = document.getElementById('subcats-' + parentSlug);
|
||
if (subRow) subRow.classList.add('visible');
|
||
|
||
// Update active badges - main stays highlighted, sub is active
|
||
document.querySelectorAll('.category-badge').forEach(badge => {
|
||
badge.classList.remove('active');
|
||
});
|
||
const mainBtn = document.querySelector('.category-badge.category-main[data-main-slug="' + parentSlug + '"]');
|
||
if (mainBtn) mainBtn.classList.add('active');
|
||
|
||
// Find and activate the sub badge
|
||
document.querySelectorAll('.category-badge.category-sub[data-parent="' + parentSlug + '"]').forEach(badge => {
|
||
const onclick = badge.getAttribute('onclick');
|
||
if (onclick && onclick.includes("'" + subSlug + "'")) {
|
||
badge.classList.add('active');
|
||
}
|
||
});
|
||
|
||
// Filter cards - only show matching subcategory
|
||
cards.forEach(card => {
|
||
const cardCategory = card.getAttribute('data-category');
|
||
card.style.display = cardCategory === subSlug ? '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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
|
||
// 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 %}
|