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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2024 lines
77 KiB
HTML
Executable File
2024 lines
77 KiB
HTML
Executable File
{% extends "base.html" %}
|
||
|
||
{% block title %}Katalog firm - Norda Biznes Partner{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
/* Events Row - siatka 2x2 */
|
||
.events-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: var(--spacing-md);
|
||
margin-bottom: var(--spacing-xl);
|
||
}
|
||
|
||
.events-row-label {
|
||
font-size: var(--font-size-xs);
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
color: rgba(255,255,255,0.9);
|
||
margin-bottom: var(--spacing-xs);
|
||
}
|
||
|
||
/* 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-md);
|
||
color: white;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--spacing-sm);
|
||
box-shadow: var(--shadow-md);
|
||
position: relative;
|
||
overflow: hidden;
|
||
text-decoration: none;
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.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-top {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: var(--spacing-sm);
|
||
}
|
||
|
||
.event-banner-icon {
|
||
font-size: 2rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.event-banner-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.event-banner-title {
|
||
font-size: var(--font-size-base);
|
||
font-weight: 700;
|
||
margin-bottom: var(--spacing-xs);
|
||
}
|
||
|
||
.event-banner-meta {
|
||
font-size: var(--font-size-xs);
|
||
opacity: 0.9;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: var(--spacing-sm);
|
||
}
|
||
|
||
.event-banner-bottom {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: var(--spacing-sm);
|
||
}
|
||
|
||
.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;
|
||
font-size: var(--font-size-xs);
|
||
}
|
||
|
||
.event-banner-action {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.event-banner .btn-light {
|
||
background: white;
|
||
color: #2E4872;
|
||
border: none;
|
||
padding: var(--spacing-xs) var(--spacing-md);
|
||
font-weight: 600;
|
||
font-size: var(--font-size-sm);
|
||
border-radius: var(--radius-btn);
|
||
text-decoration: none;
|
||
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;
|
||
}
|
||
|
||
/* Events filter buttons */
|
||
.events-filter {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-sm);
|
||
margin-bottom: var(--spacing-md);
|
||
}
|
||
|
||
.events-filter-label {
|
||
font-size: var(--font-size-sm);
|
||
color: var(--text-muted);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.events-filter-btn {
|
||
padding: var(--spacing-xs) var(--spacing-md);
|
||
border-radius: var(--radius-btn);
|
||
border: 1.5px solid #cbd5e1;
|
||
background: white;
|
||
color: var(--text-secondary);
|
||
font-size: var(--font-size-sm);
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.events-filter-btn:hover {
|
||
border-color: var(--primary);
|
||
color: var(--primary);
|
||
}
|
||
|
||
.events-filter-btn.active {
|
||
background: var(--primary);
|
||
color: white;
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.event-banner[data-event-type="external"] {
|
||
background: linear-gradient(135deg, #1a3a2a 0%, #2d5a3e 100%);
|
||
}
|
||
|
||
.event-banner[data-event-type="external"]:hover {
|
||
box-shadow: 0 10px 30px rgba(45, 90, 62, 0.25);
|
||
}
|
||
|
||
.event-banner[data-event-type="external"] .btn-light {
|
||
color: #2d5a3e;
|
||
}
|
||
|
||
.event-banner.hidden-by-filter {
|
||
display: none;
|
||
}
|
||
|
||
/* Homepage 2-column grid */
|
||
.homepage-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: var(--spacing-md);
|
||
margin-bottom: var(--spacing-lg);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.homepage-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.events-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.event-banner {
|
||
flex-direction: column;
|
||
text-align: center;
|
||
}
|
||
|
||
.event-banner-top {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.event-banner-meta {
|
||
justify-content: center;
|
||
}
|
||
|
||
.event-banner-bottom {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.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.dark-bg {
|
||
background: #1a1a2e;
|
||
}
|
||
|
||
.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-relation {
|
||
display: inline-block;
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
background: var(--background);
|
||
padding: 2px 8px;
|
||
border-radius: var(--radius);
|
||
margin-bottom: var(--spacing-xs);
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
|
||
/* What's New Widget */
|
||
.whats-new-widget {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: var(--spacing-md);
|
||
background: var(--surface);
|
||
border: 1px solid var(--border-color, #e5e7eb);
|
||
border-left: 4px solid var(--primary);
|
||
border-radius: var(--radius-lg);
|
||
padding: var(--spacing-md) var(--spacing-lg);
|
||
margin-bottom: var(--spacing-lg);
|
||
}
|
||
|
||
.whats-new-icon {
|
||
font-size: 1.5rem;
|
||
flex-shrink: 0;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.whats-new-items {
|
||
font-size: var(--font-size-sm);
|
||
color: var(--text-secondary);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.whats-new-item {
|
||
padding: 3px 0;
|
||
}
|
||
|
||
.whats-new-link {
|
||
color: var(--primary);
|
||
font-weight: 500;
|
||
font-size: var(--font-size-sm);
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
text-decoration: none;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.whats-new-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.whats-new-widget {
|
||
flex-wrap: wrap;
|
||
gap: var(--spacing-xs);
|
||
padding: var(--spacing-sm) var(--spacing-md);
|
||
}
|
||
|
||
.whats-new-icon {
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.whats-new-desc {
|
||
display: none;
|
||
}
|
||
|
||
.whats-new-extra {
|
||
display: none;
|
||
}
|
||
|
||
.whats-new-link {
|
||
width: 100%;
|
||
text-align: right;
|
||
margin-top: 0;
|
||
}
|
||
}
|
||
{% 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 %}
|
||
|
||
<!-- Co nowego na platformie -->
|
||
{% if latest_release %}
|
||
<div class="whats-new-widget">
|
||
<div class="whats-new-icon">✨</div>
|
||
<div style="flex: 1; min-width: 0;">
|
||
<div style="display: flex; align-items: baseline; gap: var(--spacing-sm); flex-wrap: wrap;">
|
||
<span style="font-weight: 600; color: var(--text-primary);">Co nowego na platformie?</span>
|
||
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);">{{ latest_release.version }} · {{ latest_release.date }}</span>
|
||
</div>
|
||
<div class="whats-new-items">
|
||
{% set all_items = (latest_release.new or []) + (latest_release.improve or []) %}
|
||
{% set starred = [] %}
|
||
{% for item in all_items %}{% if item.startswith('★') %}{% if starred.append(item) %}{% endif %}{% endif %}{% endfor %}
|
||
{% set starred_links = latest_release.get('links', {}) if latest_release.get is defined else {} %}
|
||
{% if starred %}
|
||
{% for item in starred %}
|
||
{% set item_title = item|striptags|replace('★ ', '') %}
|
||
{% set item_title_short = item_title.split(' - ')[0]|trim %}
|
||
{% set item_desc = item_title.split(' - ')[1]|trim if ' - ' in item_title else '' %}
|
||
{% set link = starred_links.get(item_title_short, '') if starred_links.get is defined else '' %}
|
||
<div class="whats-new-item{% if loop.index > 2 %} whats-new-extra{% endif %}">
|
||
★ <strong>{{ item_title_short }}</strong><span class="whats-new-desc">{{ ' - ' + item_desc if item_desc else '' }}</span>
|
||
{% if link %}
|
||
<a href="{{ link }}" style="color: var(--primary); font-weight: 500; text-decoration: none; margin-left: 6px; font-size: var(--font-size-xs);" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">Wypróbuj →</a>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
{% else %}
|
||
{% set highlight = (all_items[0] if all_items else '')|striptags %}
|
||
<div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{{ highlight }}</div>
|
||
{% endif %}
|
||
{% set total_changes = (latest_release.new|length if latest_release.new else 0) + (latest_release.improve|length if latest_release.improve else 0) + (latest_release.fix|length if latest_release.fix else 0) %}
|
||
{% set starred_count = starred|length %}
|
||
{% set hidden_starred = starred|length - 2 if starred|length > 2 else 0 %}
|
||
{% set extra_count = total_changes - starred_count + hidden_starred %}
|
||
{% if extra_count > 0 %}
|
||
<div style="margin-top: 4px;">
|
||
<a href="{{ url_for('release_notes') }}" style="color: var(--primary); font-weight: 500; text-decoration: none; font-size: var(--font-size-sm);" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">+ {{ extra_count }} {{ 'inna zmiana' if extra_count == 1 else ('inne zmiany' if extra_count < 5 else 'innych zmian') }} →</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
<a href="{{ url_for('release_notes') }}" class="whats-new-link">Zobacz wszystko →</a>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Events Filter -->
|
||
{% if upcoming_events %}
|
||
<div class="events-filter" data-animate="fadeIn">
|
||
<span class="events-filter-label">Pokaż:</span>
|
||
<button class="events-filter-btn active" data-filter="all" onclick="filterEvents('all', this)">Wszystkie</button>
|
||
<button class="events-filter-btn" data-filter="norda" onclick="filterEvents('norda', this)">🏢 Norda Biznes</button>
|
||
<button class="events-filter-btn" data-filter="external" onclick="filterEvents('external', this)">🌐 Zewnętrzne</button>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Homepage Grid: Events + Forum + New Members -->
|
||
{% if upcoming_events or latest_forum_topic or latest_admitted %}
|
||
<div class="homepage-grid" data-animate="fadeIn">
|
||
|
||
<!-- LEFT COLUMN: 2 Events -->
|
||
<div class="events-row" style="display: flex; flex-direction: column; gap: var(--spacing-md);">
|
||
{% if upcoming_events %}
|
||
{% for ue in upcoming_events[:2] %}
|
||
{% set ev = ue.event %}
|
||
<a href="{{ url_for('calendar.calendar_event', event_id=ev.id) }}" class="event-banner" data-event-type="{{ 'external' if ev.is_external else 'norda' }}" style="margin: 0;">
|
||
<div class="event-banner-top">
|
||
<div class="event-banner-icon">📅</div>
|
||
<div class="event-banner-content">
|
||
{% if loop.first %}
|
||
<div class="events-row-label">Najbliższe wydarzenia – Kto weźmie udział?</div>
|
||
{% endif %}
|
||
<div class="event-banner-title">
|
||
{{ ev.title }} →
|
||
{% if ev.is_external and ev.external_source %}
|
||
<span style="display:inline-block; background:rgba(255,255,255,0.2); color:#fff; font-size:10px; padding:2px 6px; border-radius:4px; font-weight:600; vertical-align:middle; margin-left:6px;">🌐 {{ ev.external_source }}</span>
|
||
{% endif %}
|
||
</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>
|
||
</div>
|
||
<div class="event-banner-bottom">
|
||
<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 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>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</a>
|
||
{% endfor %}
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- RIGHT COLUMN: Forum + New Members -->
|
||
<div style="display: flex; flex-direction: column; gap: var(--spacing-md);">
|
||
|
||
<!-- Latest Forum Topic -->
|
||
{% if latest_forum_topic %}
|
||
<a href="{{ url_for('forum.forum_topic', topic_id=latest_forum_topic.id) }}" style="text-decoration: none; display: block; background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--spacing-md); transition: all 0.2s; min-height: 120px;" onmouseover="this.style.borderColor='var(--primary)';this.style.boxShadow='0 2px 8px rgba(0,0,0,0.08)'" onmouseout="this.style.borderColor='var(--border)';this.style.boxShadow='none'">
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||
<span style="font-size: 1.2rem;">💬</span>
|
||
<span style="font-size: var(--font-size-xs); font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px;">Najnowszy wpis na forum</span>
|
||
</div>
|
||
<div style="font-weight: 600; color: var(--text-primary); font-size: var(--font-size-base); line-height: 1.4; margin-bottom: 8px;">
|
||
{{ latest_forum_topic.title }}
|
||
</div>
|
||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">
|
||
{{ latest_forum_topic.author.name if latest_forum_topic.author else 'Anonim' }} · {{ latest_forum_topic.created_at|local_time('%d.%m.%Y %H:%M') }}
|
||
{% if latest_forum_topic.reply_count is defined and latest_forum_topic.reply_count > 0 %}
|
||
· {{ latest_forum_topic.reply_count }} {{ 'odpowiedź' if latest_forum_topic.reply_count == 1 else ('odpowiedzi' if latest_forum_topic.reply_count < 5 else 'odpowiedzi') }}
|
||
{% endif %}
|
||
</div>
|
||
</a>
|
||
{% endif %}
|
||
|
||
<!-- New Members -->
|
||
{% if latest_admitted %}
|
||
<a href="{{ url_for('public.new_members') }}" style="text-decoration: none; display: block; background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--spacing-md); transition: all 0.2s; min-height: 120px;" onmouseover="this.style.borderColor='var(--primary)';this.style.boxShadow='0 2px 8px rgba(0,0,0,0.08)'" onmouseout="this.style.borderColor='var(--border)';this.style.boxShadow='none'">
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||
<span style="font-size: 1.2rem;">🏢</span>
|
||
<span style="font-size: var(--font-size-xs); font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px;">Nowi członkowie Izby</span>
|
||
{% if last_meeting %}
|
||
<span style="font-size: var(--font-size-xs); color: var(--text-muted);">· Rada {{ last_meeting.meeting_date.strftime('%d.%m.%Y') }}</span>
|
||
{% endif %}
|
||
</div>
|
||
<div style="display: flex; flex-direction: column; gap: 6px;">
|
||
{% for company in latest_admitted[:4] %}
|
||
<div style="display: flex; align-items: center; gap: 8px;">
|
||
<span style="color: var(--success); font-size: 14px;">✓</span>
|
||
<span style="font-weight: 500; color: var(--text-primary); font-size: var(--font-size-sm);">
|
||
{{ company.name }}
|
||
</span>
|
||
{% if company.status != 'active' %}
|
||
<span style="font-size: var(--font-size-xs); color: var(--text-muted); font-style: italic;">· profil w trakcie uzupełniania</span>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
{% if latest_admitted|length > 4 %}
|
||
<div style="font-size: var(--font-size-sm); color: var(--text-muted);">+ {{ latest_admitted|length - 4 }} więcej</div>
|
||
{% endif %}
|
||
</div>
|
||
<div style="margin-top: 8px; font-size: var(--font-size-sm); color: var(--primary); font-weight: 500;">
|
||
Zobacz wszystkich nowych członków →
|
||
</div>
|
||
</a>
|
||
{% elif last_meeting %}
|
||
<!-- No companies admitted yet at last meeting, show placeholder -->
|
||
<div style="background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--spacing-md); min-height: 120px;">
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||
<span style="font-size: 1.2rem;">🏢</span>
|
||
<span style="font-size: var(--font-size-xs); font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px;">Nowi członkowie Izby</span>
|
||
</div>
|
||
<div style="color: var(--text-muted); font-size: var(--font-size-sm);">Brak nowych firm przyjętych na ostatnim posiedzeniu Rady.</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
</div>
|
||
</div>
|
||
{% 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 — widoczny dla zalogowanych -->
|
||
{% if current_user.is_authenticated 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;">Zielony Okręg Przemysłowy Kaszubia</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 %}
|
||
<a {% if fact.source_news and fact.source_news.url %}href="{{ fact.source_news.url }}" target="_blank" rel="noopener"{% else %}href="{{ url_for('public.zopk_index') }}"{% endif %}
|
||
style="background: rgba(255,255,255,0.12); border-radius: var(--radius); padding: var(--spacing-md); text-decoration: none; color: white; display: block; transition: background 0.2s, transform 0.2s; cursor: pointer;"
|
||
onmouseover="this.style.background='rgba(255,255,255,0.22)'; this.style.transform='translateY(-2px)';"
|
||
onmouseout="this.style.background='rgba(255,255,255,0.12)'; this.style.transform='none';">
|
||
<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 %}">
|
||
{% if fact.fact_type == 'investment' %}inwestycja{% elif fact.fact_type == 'decision' %}decyzja{% elif fact.fact_type == 'event' %}wydarzenie{% elif fact.fact_type == 'milestone' %}kamień milowy{% elif fact.fact_type == 'statistic' %}dane{% elif fact.fact_type == 'partnership' %}współpraca{% elif fact.fact_type == 'project' %}projekt{% else %}fakt{% endif %}
|
||
</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|local_time('%d.%m.%Y') if fact.source_news.published_at else '' }}
|
||
<span style="float: right; opacity: 0.8;">Czytaj →</span>
|
||
</div>
|
||
{% endif %}
|
||
</a>
|
||
{% endfor %}
|
||
</div>
|
||
<div style="text-align: center; margin-top: var(--spacing-md);">
|
||
<button id="zopkMoreBtn" onclick="loadMoreFacts()" style="background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3); color: white; padding: 8px 20px; border-radius: var(--radius); cursor: pointer; font-size: var(--font-size-sm); transition: background 0.2s;"
|
||
onmouseover="this.style.background='rgba(255,255,255,0.25)';" onmouseout="this.style.background='rgba(255,255,255,0.15)';">
|
||
Pokaż więcej
|
||
</button>
|
||
</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 {{ 'dark-bg' if company.logo_dark_bg else '' }}">
|
||
<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>
|
||
{% if company_parent and company.id in company_parent %}
|
||
<span class="company-relation">marka {{ company_parent[company.id] }}</span>
|
||
{% elif company_children and company.id in company_children %}
|
||
<span class="company-relation">{{ company_children[company.id]|join(', ') }}</span>
|
||
{% endif %}
|
||
</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 %}
|
||
// Filter events by type
|
||
function filterEvents(type, btn) {
|
||
document.querySelectorAll('.events-filter-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
|
||
fetch('/api/upcoming-events?filter=' + type + '&limit=2')
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
const row = document.querySelector('.events-row');
|
||
if (!data.events || data.events.length === 0) {
|
||
row.innerHTML = '<div style="text-align:center; padding:var(--spacing-xl); color:var(--text-muted);">Brak wydarzeń w tej kategorii</div>';
|
||
return;
|
||
}
|
||
row.innerHTML = data.events.slice(0, 2).map((ev, i) => {
|
||
let typeCls = ev.is_external ? 'external' : 'norda';
|
||
let sourceBadge = ev.is_external && ev.external_source
|
||
? '<span style="display:inline-block;background:rgba(255,255,255,0.2);color:#fff;font-size:10px;padding:2px 6px;border-radius:4px;font-weight:600;vertical-align:middle;margin-left:6px;">🌐 ' + ev.external_source + '</span>'
|
||
: '';
|
||
let accessBadge = '';
|
||
if (ev.access_level === 'admin_only') accessBadge = '<span style="display:inline-block;background:#ef4444;color:#fff;font-size:10px;padding:2px 6px;border-radius:4px;font-weight:600;vertical-align:middle;margin-left:6px;">UKRYTE</span>';
|
||
else if (ev.access_level === 'rada_only') accessBadge = '<span style="display:inline-block;background:#f59e0b;color:#92400e;font-size:10px;padding:2px 6px;border-radius:4px;font-weight:600;vertical-align:middle;margin-left:6px;">IZBA</span>';
|
||
|
||
let timePart = ev.time ? '<span>🕕 ' + ev.time + '</span>' : '';
|
||
let locPart = ev.location ? '<span>📍 ' + ev.location + '</span>' : '';
|
||
let cnt = ev.attendee_count;
|
||
let cntWord = cnt === 1 ? 'osoba' : (cnt >= 2 && cnt <= 4 ? 'osoby' : 'osób');
|
||
|
||
let actionHtml = '';
|
||
if (ev.user_registered) {
|
||
actionHtml = '<span class="btn-light btn-registered">✓ Jesteś zapisany/a</span>';
|
||
} else if (ev.user_can_attend) {
|
||
actionHtml = '<button type="button" class="btn-light" onclick="rsvpAndGo(event, ' + ev.id + ')">Zapisz się →</button>';
|
||
} else if (ev.access_level === 'rada_only') {
|
||
actionHtml = '<span class="btn-light" style="background:#fef3c7;color:#92400e;border:1px solid #fde68a;">🔒 Rada Izby</span>';
|
||
}
|
||
|
||
let label = i === 0 ? '<div class="events-row-label">Najbliższe wydarzenia – Kto weźmie udział?</div>' : '';
|
||
|
||
return '<a href="' + ev.url + '" class="event-banner" data-event-type="' + typeCls + '">' +
|
||
'<div class="event-banner-top"><div class="event-banner-icon">📅</div><div class="event-banner-content">' +
|
||
label +
|
||
'<div class="event-banner-title">' + ev.title + ' →' + sourceBadge + accessBadge + '</div>' +
|
||
'<div class="event-banner-meta"><span>📆 ' + ev.date + ' (' + ev.day + ')</span>' + timePart + locPart + '</div>' +
|
||
'</div></div>' +
|
||
'<div class="event-banner-bottom">' +
|
||
'<div class="event-banner-attendees">👥 Zapisanych: ' + cnt + ' ' + cntWord + '</div>' +
|
||
'<div class="event-banner-action">' + actionHtml + '</div>' +
|
||
'</div></a>';
|
||
}).join('');
|
||
});
|
||
}
|
||
|
||
// 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();
|
||
}
|
||
});
|
||
|
||
// ZOPK "Czy wiesz, że..." — load more facts
|
||
let zopkOffset = 3;
|
||
async function loadMoreFacts() {
|
||
const btn = document.getElementById('zopkMoreBtn');
|
||
btn.textContent = 'Ładowanie...';
|
||
btn.disabled = true;
|
||
try {
|
||
const resp = await fetch('/api/zopk-facts?offset=' + zopkOffset);
|
||
const data = await resp.json();
|
||
if (data.facts && data.facts.length > 0) {
|
||
const grid = btn.closest('div[data-animate]').querySelector('[style*="display: grid"]');
|
||
const typeColors = {investment:'rgba(16,185,129,0.3)',event:'rgba(59,130,246,0.3)',decision:'rgba(245,158,11,0.3)',milestone:'rgba(139,92,246,0.3)'};
|
||
data.facts.forEach(f => {
|
||
const card = document.createElement('a');
|
||
card.href = f.source_url || '/zopk';
|
||
card.target = '_blank';
|
||
card.rel = 'noopener';
|
||
card.style.cssText = 'background:rgba(255,255,255,0.12);border-radius:var(--radius);padding:var(--spacing-md);text-decoration:none;color:white;display:block;transition:background 0.2s,transform 0.2s;cursor:pointer;opacity:0;';
|
||
card.onmouseover = function(){this.style.background='rgba(255,255,255,0.22)';this.style.transform='translateY(-2px)';};
|
||
card.onmouseout = function(){this.style.background='rgba(255,255,255,0.12)';this.style.transform='none';};
|
||
const color = typeColors[f.type] || 'rgba(255,255,255,0.2)';
|
||
card.innerHTML = '<span style="display:inline-block;padding:1px 8px;border-radius:4px;font-size:11px;font-weight:600;margin-bottom:var(--spacing-xs);background:'+color+';">'+f.type_label+'</span>'
|
||
+ '<p style="font-size:var(--font-size-sm);line-height:1.5;margin:0;opacity:0.95;">'+f.text+(f.text.length>=200?'...':'')+'</p>'
|
||
+ '<div style="font-size:11px;margin-top:var(--spacing-xs);opacity:0.7;">'+f.source_name+' • '+f.source_date+'<span style="float:right;opacity:0.8;">Czytaj →</span></div>';
|
||
grid.appendChild(card);
|
||
requestAnimationFrame(() => { card.style.transition = 'opacity 0.4s'; card.style.opacity = '1'; });
|
||
});
|
||
zopkOffset += data.facts.length;
|
||
if (!data.has_more) {
|
||
btn.textContent = 'To wszystko';
|
||
btn.disabled = true;
|
||
btn.style.opacity = '0.5';
|
||
} else {
|
||
btn.textContent = 'Pokaż więcej';
|
||
btn.disabled = false;
|
||
}
|
||
} else {
|
||
btn.textContent = 'Brak więcej faktów';
|
||
btn.disabled = true;
|
||
btn.style.opacity = '0.5';
|
||
}
|
||
} catch(e) {
|
||
btn.textContent = 'Pokaż więcej';
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
{% endblock %}
|