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
Adds direct shortcut to company edit (MANAGER) or company profile (EMPLOYEE) in the user avatar dropdown. Addresses feedback that editing company data required searching for own company in the directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2693 lines
110 KiB
HTML
Executable File
2693 lines
110 KiB
HTML
Executable File
<!DOCTYPE html>
|
|
<html lang="pl">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="description" content="{% block meta_description %}Norda Biznes Partner - katalog firm członkowskich Izby Gospodarczej Norda Biznes z Wejherowa. Networking i współpraca biznesowa w województwie pomorskim.{% endblock %}">
|
|
<meta name="author" content="Norda Biznes">
|
|
<meta name="robots" content="index, follow">
|
|
<meta name="page-view-id" content="{{ page_view_id|default('') }}">
|
|
{% if company_id %}<meta name="company-id" content="{{ company_id }}">{% endif %}
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
|
|
<title>{% block title %}Norda Biznes Partner{% endblock %}</title>
|
|
|
|
<!-- Canonical URL -->
|
|
<link rel="canonical" href="{% block canonical_url %}{{ request.url_root.rstrip('/') }}{{ request.path }}{% endblock %}">
|
|
|
|
<!-- Favicon - Gwiazda Nordy (kompas) -->
|
|
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='img/favicon.svg') }}">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='img/favicon-32.png') }}">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='img/favicon-16.png') }}">
|
|
<link rel="alternate icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
|
|
<!-- Apple Touch Icon (iOS) -->
|
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='img/apple-touch-icon.png') }}">
|
|
<!-- Web Manifest (PWA) -->
|
|
<link rel="manifest" href="{{ url_for('static', filename='site.webmanifest') }}">
|
|
<meta name="theme-color" content="#233e6d">
|
|
|
|
<!-- RSS Feeds -->
|
|
<link rel="alternate" type="application/rss+xml" title="Norda Biznes — Wydarzenia" href="/feed/events.xml">
|
|
<link rel="alternate" type="application/rss+xml" title="Norda Biznes — Aktualności" href="/feed/news.xml">
|
|
<link rel="alternate" type="application/rss+xml" title="Norda Biznes — PEJ" href="/feed/pej.xml">
|
|
|
|
<!-- Preload critical resources for LCP optimization -->
|
|
<link rel="preload" href="{{ url_for('static', filename='img/favicon-512.png') }}" as="image">
|
|
|
|
<!-- Fonts - Poppins (norda-biznes.info style) -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
<!-- Styles -->
|
|
<style>
|
|
/* ============================================================
|
|
* RESET & BASE STYLES
|
|
* ============================================================ */
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:root {
|
|
/* Colors - NordaBiz brand palette (norda-biznes.info style) */
|
|
--primary: #2E4872;
|
|
--primary-dark: #1e3050;
|
|
--primary-light: #4a6999;
|
|
--secondary: #64748b;
|
|
--success: #10b981;
|
|
--warning: #f59e0b;
|
|
--error: #ef4444;
|
|
--background: #EDF0F5;
|
|
--section-bg: #EDF0F5;
|
|
--surface: #ffffff;
|
|
--text-primary: #303030;
|
|
--text-secondary: #464646;
|
|
--border: #e0e4eb;
|
|
|
|
/* Spacing */
|
|
--spacing-xs: 0.25rem;
|
|
--spacing-sm: 0.5rem;
|
|
--spacing-md: 1rem;
|
|
--spacing-lg: 1.5rem;
|
|
--spacing-xl: 2rem;
|
|
--spacing-2xl: 3rem;
|
|
|
|
/* Typography - Poppins */
|
|
--font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
--font-size-sm: 0.875rem;
|
|
--font-size-base: 1rem;
|
|
--font-size-lg: 1.125rem;
|
|
--font-size-xl: 1.25rem;
|
|
--font-size-2xl: 1.5rem;
|
|
--font-size-3xl: 1.875rem;
|
|
|
|
/* Shadows */
|
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
|
|
/* Border radius */
|
|
--radius-sm: 0.25rem;
|
|
--radius: 0.5rem;
|
|
--radius-lg: 0.75rem;
|
|
--radius-xl: 1rem;
|
|
--radius-btn: 12px 4px 12px 4px;
|
|
|
|
/* Transitions */
|
|
--transition: all 0.2s ease-in-out;
|
|
}
|
|
|
|
html, body {
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
body {
|
|
font-family: var(--font-family);
|
|
font-size: var(--font-size-base);
|
|
line-height: 1.6;
|
|
color: var(--text-primary);
|
|
background-color: var(--background);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* ============================================================
|
|
* LAYOUT
|
|
* ============================================================ */
|
|
.container {
|
|
width: 100%;
|
|
max-width: 1280px;
|
|
margin: 0 auto;
|
|
padding: 0 var(--spacing-md);
|
|
}
|
|
|
|
.container-narrow {
|
|
max-width: 768px;
|
|
}
|
|
|
|
main {
|
|
flex: 1;
|
|
padding: var(--spacing-xl) 0;
|
|
}
|
|
|
|
/* ============================================================
|
|
* HEADER & NAVIGATION
|
|
* ============================================================ */
|
|
header {
|
|
background-color: var(--surface);
|
|
border-bottom: none;
|
|
box-shadow: 0 2px 10px rgba(46, 72, 114, 0.08);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
}
|
|
|
|
nav {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: var(--spacing-md) 0;
|
|
position: relative;
|
|
}
|
|
|
|
.nav-brand {
|
|
font-size: var(--font-size-xl);
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.nav-brand:hover {
|
|
color: var(--primary-dark);
|
|
}
|
|
|
|
.nav-title {
|
|
position: absolute;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
font-size: var(--font-size-xl);
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.nav-menu {
|
|
display: flex;
|
|
gap: var(--spacing-lg);
|
|
align-items: center;
|
|
list-style: none;
|
|
}
|
|
|
|
.nav-link {
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
transition: var(--transition);
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.nav-link:hover {
|
|
color: var(--primary);
|
|
background-color: var(--background);
|
|
}
|
|
|
|
.nav-badge-beta {
|
|
position: relative;
|
|
display: inline-block;
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
color: white;
|
|
background: linear-gradient(135deg, #ef4444, #f97316);
|
|
padding: 1px 5px;
|
|
border-radius: 3px;
|
|
line-height: 1.2;
|
|
letter-spacing: 0.5px;
|
|
transform: rotate(-12deg);
|
|
vertical-align: super;
|
|
margin-left: 2px;
|
|
text-transform: uppercase;
|
|
box-shadow: 0 1px 3px rgba(239,68,68,0.4);
|
|
}
|
|
|
|
.nav-link.active {
|
|
color: var(--primary);
|
|
}
|
|
|
|
/* Navigation badge (unread messages/notifications) */
|
|
.nav-link-with-badge {
|
|
position: relative;
|
|
}
|
|
|
|
.nav-badge {
|
|
position: absolute;
|
|
top: -4px;
|
|
right: -8px;
|
|
background: var(--error);
|
|
color: white;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
min-width: 18px;
|
|
height: 18px;
|
|
border-radius: 9px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0 5px;
|
|
}
|
|
|
|
/* Notifications dropdown */
|
|
.notifications-dropdown {
|
|
position: relative;
|
|
}
|
|
|
|
.notifications-trigger {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
color: var(--text-secondary);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
border-radius: var(--radius);
|
|
transition: var(--transition);
|
|
font-size: var(--font-size-base);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.notifications-trigger:hover {
|
|
color: var(--primary);
|
|
background-color: var(--background);
|
|
}
|
|
|
|
.notifications-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
|
|
.notifications-menu {
|
|
display: none;
|
|
position: absolute;
|
|
top: 100%;
|
|
right: 0;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow-lg);
|
|
min-width: 320px;
|
|
max-width: 400px;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
z-index: 200;
|
|
}
|
|
|
|
.notifications-menu.show {
|
|
display: block;
|
|
}
|
|
|
|
.notifications-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: var(--spacing-md);
|
|
border-bottom: 1px solid var(--border);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.notifications-mark-all {
|
|
background: none;
|
|
border: none;
|
|
color: var(--primary);
|
|
cursor: pointer;
|
|
font-size: var(--font-size-sm);
|
|
padding: 0;
|
|
}
|
|
|
|
.notifications-mark-all:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.notification-item {
|
|
display: block;
|
|
padding: var(--spacing-md);
|
|
border-bottom: 1px solid var(--border);
|
|
text-decoration: none;
|
|
color: var(--text-primary);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.notification-item:hover {
|
|
background-color: var(--background);
|
|
}
|
|
|
|
.notification-item.unread {
|
|
background-color: #f0f7ff;
|
|
}
|
|
|
|
.notification-item.unread:hover {
|
|
background-color: #e6f0ff;
|
|
}
|
|
|
|
.notification-title {
|
|
font-weight: 600;
|
|
font-size: var(--font-size-sm);
|
|
margin-bottom: var(--spacing-xs);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
}
|
|
|
|
.notification-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.notification-message {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
margin-bottom: var(--spacing-xs);
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.notification-time {
|
|
font-size: 11px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.notification-type-icon {
|
|
width: 16px;
|
|
height: 16px;
|
|
margin-right: var(--spacing-xs);
|
|
}
|
|
|
|
.notifications-empty {
|
|
padding: var(--spacing-xl);
|
|
text-align: center;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.notifications-footer {
|
|
padding: var(--spacing-sm);
|
|
text-align: center;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.notifications-footer a {
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
/* Admin dropdown menu */
|
|
.nav-dropdown {
|
|
position: relative;
|
|
}
|
|
|
|
.nav-dropdown-menu {
|
|
display: none;
|
|
position: absolute;
|
|
top: 100%;
|
|
right: 0;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow-lg);
|
|
min-width: 180px;
|
|
padding: var(--spacing-xs) 0;
|
|
z-index: 200;
|
|
list-style: none;
|
|
}
|
|
|
|
.nav-dropdown:hover .nav-dropdown-menu {
|
|
display: block;
|
|
}
|
|
|
|
.nav-dropdown-menu li a {
|
|
display: block;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
color: var(--text-primary);
|
|
text-decoration: none;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.nav-dropdown-menu li a:hover {
|
|
background: var(--background);
|
|
color: var(--primary);
|
|
}
|
|
|
|
/* Admin Bar */
|
|
.admin-bar {
|
|
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
padding: 0 var(--spacing-md);
|
|
}
|
|
|
|
.admin-bar-inner {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
}
|
|
|
|
.admin-bar-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
color: rgba(255,255,255,0.7);
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
padding: 10px 12px 10px 0;
|
|
border-right: 1px solid rgba(255,255,255,0.15);
|
|
margin-right: var(--spacing-xs);
|
|
}
|
|
|
|
.admin-bar-label svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.admin-dropdown {
|
|
position: relative;
|
|
}
|
|
|
|
.admin-dropdown-trigger {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 10px 14px;
|
|
color: rgba(255,255,255,0.85);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
background: transparent;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.admin-dropdown-trigger:hover {
|
|
background: rgba(255,255,255,0.1);
|
|
color: #fff;
|
|
}
|
|
|
|
.admin-dropdown-trigger svg {
|
|
width: 12px;
|
|
height: 12px;
|
|
opacity: 0.6;
|
|
transition: transform 0.15s ease;
|
|
}
|
|
|
|
.admin-dropdown:hover .admin-dropdown-trigger svg {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.admin-dropdown-menu {
|
|
display: none;
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow-lg);
|
|
min-width: 180px;
|
|
padding: var(--spacing-xs) 0;
|
|
z-index: 300;
|
|
}
|
|
|
|
.admin-dropdown:hover .admin-dropdown-menu,
|
|
.admin-dropdown.open .admin-dropdown-menu {
|
|
display: block;
|
|
}
|
|
|
|
.admin-dropdown-menu a {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 14px;
|
|
color: var(--text-primary);
|
|
text-decoration: none;
|
|
font-size: 13px;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.admin-dropdown-menu a:hover {
|
|
background: var(--background);
|
|
color: var(--primary);
|
|
}
|
|
|
|
.admin-dropdown-menu a svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
color: var(--text-secondary);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.admin-dropdown-menu a:hover svg {
|
|
color: var(--primary);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.admin-bar-inner {
|
|
flex-wrap: wrap;
|
|
}
|
|
.admin-bar-label {
|
|
font-size: 10px;
|
|
padding: 8px 8px 8px 0;
|
|
}
|
|
.admin-dropdown-trigger {
|
|
font-size: 12px;
|
|
padding: 8px 10px;
|
|
white-space: nowrap;
|
|
}
|
|
.admin-bar-inner {
|
|
position: relative;
|
|
}
|
|
.admin-dropdown-menu {
|
|
position: fixed;
|
|
top: auto;
|
|
left: 8px;
|
|
right: 8px;
|
|
width: auto;
|
|
max-height: 70vh;
|
|
overflow-y: auto;
|
|
z-index: 9999;
|
|
border-radius: var(--radius);
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
|
|
}
|
|
}
|
|
|
|
/* Mobile menu toggle */
|
|
.nav-toggle {
|
|
display: none;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: var(--spacing-sm);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.nav-toggle {
|
|
display: block;
|
|
}
|
|
|
|
.nav-menu {
|
|
display: none;
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
background-color: var(--surface);
|
|
flex-direction: column;
|
|
padding: var(--spacing-md);
|
|
border-top: 1px solid var(--border);
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
|
z-index: 99;
|
|
max-height: 70vh;
|
|
overflow-y: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
.nav-menu.active {
|
|
display: flex;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.notifications-dropdown {
|
|
position: static;
|
|
}
|
|
|
|
.notifications-menu {
|
|
position: fixed;
|
|
top: auto;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
min-width: unset;
|
|
max-width: unset;
|
|
width: 100%;
|
|
max-height: 60vh;
|
|
border-radius: var(--radius) var(--radius) 0 0;
|
|
box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
|
|
z-index: 1000;
|
|
}
|
|
}
|
|
|
|
/* ============================================================
|
|
* BUTTONS
|
|
* ============================================================ */
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--spacing-sm);
|
|
padding: var(--spacing-sm) var(--spacing-lg);
|
|
font-size: var(--font-size-base);
|
|
font-weight: 500;
|
|
text-decoration: none;
|
|
border-radius: var(--radius-btn);
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.btn-primary {
|
|
background-color: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background-color: var(--primary-dark);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background-color: var(--secondary);
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background-color: #475569;
|
|
}
|
|
|
|
.btn-outline {
|
|
background-color: transparent;
|
|
color: var(--primary);
|
|
border: 2px solid var(--primary);
|
|
}
|
|
|
|
.btn-outline:hover {
|
|
background-color: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.btn-sm {
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.btn-lg {
|
|
padding: var(--spacing-md) var(--spacing-xl);
|
|
font-size: var(--font-size-lg);
|
|
}
|
|
|
|
/* ============================================================
|
|
* FLASH MESSAGES
|
|
* ============================================================ */
|
|
.flash-messages {
|
|
position: fixed;
|
|
top: var(--spacing-xl);
|
|
right: var(--spacing-xl);
|
|
z-index: 1000;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.flash {
|
|
padding: var(--spacing-md);
|
|
margin-bottom: var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow-lg);
|
|
display: flex;
|
|
align-items: start;
|
|
gap: var(--spacing-md);
|
|
animation: slideIn 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
transform: translateX(100%);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.flash-success {
|
|
background-color: var(--success);
|
|
color: white;
|
|
}
|
|
|
|
.flash-error {
|
|
background-color: var(--error);
|
|
color: white;
|
|
}
|
|
|
|
.flash-warning {
|
|
background-color: var(--warning);
|
|
color: white;
|
|
}
|
|
|
|
.flash-info {
|
|
background-color: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.flash-close {
|
|
background: none;
|
|
border: none;
|
|
color: inherit;
|
|
font-size: var(--font-size-xl);
|
|
cursor: pointer;
|
|
margin-left: auto;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.flash-close:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* ============================================================
|
|
* DEVELOPMENT NOTICE BANNER
|
|
* ============================================================ */
|
|
.dev-notice {
|
|
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
|
|
border-bottom: 2px solid #3b82f6;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
text-align: center;
|
|
font-size: var(--font-size-sm);
|
|
color: #1e3a5f;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.dev-notice-icon {
|
|
font-size: 1.2em;
|
|
}
|
|
|
|
.dev-notice-text {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.dev-notice-close {
|
|
background: none;
|
|
border: none;
|
|
color: #1e3a5f;
|
|
font-size: 1.2em;
|
|
cursor: pointer;
|
|
padding: 0 var(--spacing-sm);
|
|
opacity: 0.6;
|
|
transition: var(--transition);
|
|
margin-left: var(--spacing-md);
|
|
}
|
|
|
|
.dev-notice-close:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* ============================================================
|
|
* FOOTER
|
|
* ============================================================ */
|
|
footer {
|
|
background-color: #2E4872;
|
|
color: white;
|
|
padding: 60px 0 30px;
|
|
margin-top: auto;
|
|
}
|
|
|
|
.footer-content {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: var(--spacing-xl);
|
|
}
|
|
|
|
.footer-section h3 {
|
|
margin-bottom: var(--spacing-md);
|
|
font-size: var(--font-size-lg);
|
|
}
|
|
|
|
.footer-section p,
|
|
.footer-section a {
|
|
color: #94a3b8;
|
|
text-decoration: none;
|
|
display: block;
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.footer-section a:hover {
|
|
color: white;
|
|
}
|
|
|
|
.footer-bottom {
|
|
border-top: 1px solid #334155;
|
|
margin-top: var(--spacing-xl);
|
|
padding-top: var(--spacing-md);
|
|
text-align: center;
|
|
color: #94a3b8;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.footer-creator {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--spacing-sm);
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.footer-creator span {
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.creator-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.creator-logo {
|
|
height: 28px;
|
|
width: auto;
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
.creator-link:hover .creator-logo {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.footer-section a {
|
|
color: #94a3b8;
|
|
}
|
|
|
|
/* ============================================================
|
|
* UTILITIES
|
|
* ============================================================ */
|
|
.text-center { text-align: center; }
|
|
.text-muted { color: var(--text-secondary); }
|
|
.mt-1 { margin-top: var(--spacing-md); }
|
|
.mt-2 { margin-top: var(--spacing-lg); }
|
|
.mt-3 { margin-top: var(--spacing-xl); }
|
|
.mb-1 { margin-bottom: var(--spacing-md); }
|
|
.mb-2 { margin-bottom: var(--spacing-lg); }
|
|
.mb-3 { margin-bottom: var(--spacing-xl); }
|
|
|
|
|
|
/* ============================================================
|
|
* USER DROPDOWN MENU
|
|
* ============================================================ */
|
|
.user-dropdown {
|
|
position: relative;
|
|
}
|
|
|
|
.user-trigger {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
border-radius: var(--radius);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.user-trigger:hover {
|
|
background-color: var(--background);
|
|
}
|
|
|
|
.user-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.user-name {
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
max-width: 100px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.user-chevron {
|
|
color: var(--text-secondary);
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.user-dropdown.active .user-chevron {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.user-menu {
|
|
display: none;
|
|
position: absolute;
|
|
top: calc(100% + 4px);
|
|
right: 0;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow-lg);
|
|
min-width: 220px;
|
|
z-index: 200;
|
|
}
|
|
|
|
.user-menu.show {
|
|
display: block;
|
|
}
|
|
|
|
/* Mobile: bottom sheet */
|
|
@media (max-width: 768px) {
|
|
.user-menu-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(0,0,0,0.4);
|
|
z-index: 9998;
|
|
}
|
|
.user-menu-overlay.show { display: block; }
|
|
|
|
.user-menu {
|
|
display: block !important;
|
|
position: fixed;
|
|
top: auto;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
width: 100%;
|
|
min-width: 100%;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
border-radius: 16px 16px 0 0;
|
|
border: none;
|
|
box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
|
|
z-index: 9999;
|
|
padding-bottom: env(safe-area-inset-bottom, 16px);
|
|
visibility: hidden;
|
|
transform: translateY(100%);
|
|
transition: transform 0.25s ease-out, visibility 0s 0.25s;
|
|
pointer-events: none;
|
|
}
|
|
.user-menu.show {
|
|
visibility: visible;
|
|
transform: translateY(0);
|
|
transition: transform 0.25s ease-out, visibility 0s 0s;
|
|
pointer-events: auto;
|
|
}
|
|
.user-menu-handle {
|
|
display: block;
|
|
width: 36px;
|
|
height: 4px;
|
|
background: #d1d5db;
|
|
border-radius: 2px;
|
|
margin: 10px auto 4px;
|
|
}
|
|
.user-menu-mobile-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 8px 16px 12px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.user-menu-mobile-header .user-avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
font-size: 16px;
|
|
}
|
|
.user-menu-mobile-name {
|
|
font-weight: 600;
|
|
font-size: 15px;
|
|
color: var(--text-primary);
|
|
}
|
|
.user-menu-item {
|
|
padding: 14px 16px;
|
|
font-size: 15px;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 769px) {
|
|
.user-menu-overlay { display: none !important; }
|
|
.user-menu-handle { display: none; }
|
|
.user-menu-mobile-header { display: none; }
|
|
}
|
|
|
|
.user-menu-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
color: var(--text-primary);
|
|
text-decoration: none;
|
|
transition: var(--transition);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.user-menu-item:hover {
|
|
background-color: var(--background);
|
|
color: var(--primary);
|
|
}
|
|
|
|
.user-menu-item svg {
|
|
color: var(--text-secondary);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.user-menu-item:hover svg {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.user-menu-item-badge {
|
|
position: relative;
|
|
}
|
|
|
|
.user-menu-badge {
|
|
background: var(--error);
|
|
color: white;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
padding: 2px 6px;
|
|
border-radius: 10px;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.user-menu-divider {
|
|
height: 1px;
|
|
background: var(--border);
|
|
margin: var(--spacing-xs) 0;
|
|
}
|
|
|
|
.user-menu-section {
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.user-menu-logout {
|
|
color: var(--error);
|
|
}
|
|
|
|
.user-menu-logout:hover {
|
|
background-color: #fef2f2;
|
|
color: var(--error);
|
|
}
|
|
|
|
.user-menu-logout svg {
|
|
color: var(--error);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.user-name {
|
|
display: none;
|
|
}
|
|
.user-chevron {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* ============================================================
|
|
* SCROLL ANIMATIONS (Sprint 4)
|
|
* ============================================================ */
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@keyframes fadeInLeft {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(-30px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
@keyframes fadeInRight {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(30px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
@keyframes fadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(30px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@keyframes fadeOut {
|
|
from {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
to {
|
|
opacity: 0;
|
|
transform: translateX(20px);
|
|
}
|
|
}
|
|
|
|
@keyframes scaleIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.9);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
/* Elements waiting for animation */
|
|
[data-animate] {
|
|
opacity: 0;
|
|
}
|
|
|
|
/* Animation classes applied by IntersectionObserver */
|
|
.animate-fadeIn {
|
|
animation: fadeIn 0.6s ease forwards;
|
|
}
|
|
|
|
.animate-fadeInLeft {
|
|
animation: fadeInLeft 0.6s ease forwards;
|
|
}
|
|
|
|
.animate-fadeInRight {
|
|
animation: fadeInRight 0.6s ease forwards;
|
|
}
|
|
|
|
.animate-fadeInUp {
|
|
animation: fadeInUp 0.6s ease forwards;
|
|
}
|
|
|
|
.animate-scaleIn {
|
|
animation: scaleIn 0.5s ease forwards;
|
|
}
|
|
|
|
/* Stagger delays for card grids */
|
|
[data-animate-delay="1"] { animation-delay: 0.1s; }
|
|
[data-animate-delay="2"] { animation-delay: 0.2s; }
|
|
[data-animate-delay="3"] { animation-delay: 0.3s; }
|
|
[data-animate-delay="4"] { animation-delay: 0.4s; }
|
|
[data-animate-delay="5"] { animation-delay: 0.5s; }
|
|
[data-animate-delay="6"] { animation-delay: 0.6s; }
|
|
|
|
/* Respect reduced motion preference */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
[data-animate] {
|
|
opacity: 1;
|
|
}
|
|
.animate-fadeIn,
|
|
.animate-fadeInLeft,
|
|
.animate-fadeInRight,
|
|
.animate-fadeInUp,
|
|
.animate-scaleIn {
|
|
animation: none;
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
{% if is_staging %}
|
|
/* ============================================================
|
|
* STAGING ENVIRONMENT INDICATORS
|
|
* ============================================================ */
|
|
.staging-banner {
|
|
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
|
color: white;
|
|
text-align: center;
|
|
padding: 6px var(--spacing-md);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
z-index: 1000;
|
|
position: relative;
|
|
}
|
|
|
|
.nav-test-badge {
|
|
display: inline-block;
|
|
background: #f97316;
|
|
color: white;
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
padding: 1px 5px;
|
|
border-radius: 3px;
|
|
margin-left: 4px;
|
|
vertical-align: middle;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
animation: staging-pulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes staging-pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.7; }
|
|
}
|
|
|
|
.staging-panel-toggle {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
width: 48px;
|
|
height: 48px;
|
|
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
box-shadow: var(--shadow-lg);
|
|
z-index: 9999;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.staging-panel-toggle:hover {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.staging-panel-toggle svg {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
.staging-panel {
|
|
position: fixed;
|
|
bottom: 80px;
|
|
right: 20px;
|
|
width: 320px;
|
|
max-height: 400px;
|
|
background: var(--surface);
|
|
border: 2px solid #f97316;
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-lg);
|
|
z-index: 9998;
|
|
display: none;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.staging-panel.open {
|
|
display: block;
|
|
}
|
|
|
|
.staging-panel-header {
|
|
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
|
color: white;
|
|
padding: 12px 16px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.staging-panel-close {
|
|
background: none;
|
|
border: none;
|
|
color: white;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
}
|
|
|
|
.staging-panel-body {
|
|
padding: 12px 16px;
|
|
overflow-y: auto;
|
|
max-height: 320px;
|
|
}
|
|
|
|
.staging-feature-item {
|
|
padding: 10px 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.staging-feature-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.staging-feature-name {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.staging-feature-status {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: #f97316;
|
|
display: inline-block;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.staging-feature-desc {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
margin-top: 4px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.staging-panel {
|
|
width: calc(100vw - 40px);
|
|
right: 20px;
|
|
bottom: 76px;
|
|
}
|
|
}
|
|
{% endif %}
|
|
|
|
/* Owner-only indicator — pomarańczowa kropka */
|
|
.owner-only::after {
|
|
content: '';
|
|
display: inline-block;
|
|
width: 6px;
|
|
height: 6px;
|
|
background: #f59e0b;
|
|
border-radius: 50%;
|
|
margin-left: 6px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
/* ============================================================
|
|
* PWA INSTALL BANNER (mobile-only)
|
|
* ============================================================ */
|
|
.pwa-smart-banner {
|
|
display: none;
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background: var(--surface);
|
|
border-top: 1px solid var(--border);
|
|
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
|
|
padding: 12px 16px;
|
|
z-index: 9999;
|
|
align-items: center;
|
|
gap: 12px;
|
|
transform: translateY(100%);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.pwa-smart-banner.visible {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.pwa-smart-banner-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.pwa-smart-banner-icon img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.pwa-smart-banner-text {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.pwa-smart-banner-text strong {
|
|
display: block;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.pwa-smart-banner-text span {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.pwa-smart-banner-action {
|
|
background: var(--primary);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 20px;
|
|
padding: 8px 16px;
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 600;
|
|
font-family: var(--font-family);
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.pwa-smart-banner-close {
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
flex-shrink: 0;
|
|
line-height: 1;
|
|
}
|
|
|
|
/* Footer PWA link — mobile only */
|
|
.footer-pwa-link {
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.pwa-smart-banner {
|
|
display: flex;
|
|
}
|
|
.footer-pwa-link {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
/* Hide banner in standalone (already installed) */
|
|
@media (display-mode: standalone) {
|
|
.pwa-smart-banner {
|
|
display: none !important;
|
|
}
|
|
.footer-pwa-link {
|
|
display: none !important;
|
|
}
|
|
}
|
|
|
|
{% block extra_css %}{% endblock %}
|
|
</style>
|
|
|
|
<!-- Open Graph -->
|
|
<meta property="og:title" content="{% block og_title %}Norda Biznes Partner{% endblock %}">
|
|
<meta property="og:description" content="{% block og_description %}Katalog firm członkowskich Izby Gospodarczej Norda Biznes z Wejherowa. Networking i współpraca biznesowa w województwie pomorskim.{% endblock %}">
|
|
<meta property="og:image" content="{{ url_for('static', filename='img/favicon-512.png', _external=True) }}">
|
|
<meta property="og:url" content="{{ request.url }}">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:site_name" content="Norda Biznes Partner">
|
|
<meta property="og:locale" content="pl_PL">
|
|
|
|
<!-- JSON-LD Structured Data -->
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "Organization",
|
|
"name": "Stowarzyszenie Norda Biznes",
|
|
"url": "https://nordabiznes.pl",
|
|
"logo": "{{ url_for('static', filename='img/favicon-512.png', _external=True) }}",
|
|
"address": {
|
|
"@type": "PostalAddress",
|
|
"streetAddress": "ul. 12 Marca 238/5",
|
|
"addressLocality": "Wejherowo",
|
|
"postalCode": "84-200",
|
|
"addressCountry": "PL"
|
|
},
|
|
"sameAs": [
|
|
"https://www.facebook.com/profile.php?id=100057396041901",
|
|
"https://norda-biznes.info"
|
|
]
|
|
}
|
|
</script>
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "WebSite",
|
|
"name": "Norda Biznes Partner",
|
|
"url": "https://nordabiznes.pl"
|
|
}
|
|
</script>
|
|
|
|
{% block head_extra %}{% endblock %}
|
|
|
|
<!-- Analytics Tracker -->
|
|
<script src="{{ url_for('static', filename='js/analytics-tracker.js') }}" defer></script>
|
|
</head>
|
|
<body>
|
|
{% if is_staging %}
|
|
<!-- Staging Environment Banner -->
|
|
<div class="staging-banner">⚠ STAGING — Środowisko testowe</div>
|
|
{% endif %}
|
|
|
|
<!-- Header -->
|
|
<header>
|
|
<div class="container">
|
|
<nav role="navigation" aria-label="Main navigation">
|
|
<a href="{{ url_for('index') }}" class="nav-brand" aria-label="Norda Biznes Home">
|
|
<img src="{{ url_for('static', filename='img/norda-logo.svg') }}" alt="Norda Biznes" height="36" style="object-fit: contain;">
|
|
</a>
|
|
{% if not current_user.is_authenticated %}<span class="nav-title">Norda Biznes Partner</span>{% endif %}
|
|
|
|
<button class="nav-toggle" aria-label="Toggle navigation" onclick="toggleMobileMenu()">
|
|
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M3 12h18M3 6h18M3 18h18"/>
|
|
</svg>
|
|
</button>
|
|
|
|
<ul class="nav-menu" id="navMenu">
|
|
{% if current_user.is_authenticated %}
|
|
<li><a href="{{ url_for('index') }}" class="nav-link {% if request.endpoint == 'index' %}active{% endif %}">Firmy</a></li>
|
|
<!-- NordaGPT - główna pozycja -->
|
|
<li><a href="{{ url_for('chat') }}" class="nav-link {% if request.endpoint == 'chat' %}active{% endif %}">NordaGPT</a></li>
|
|
|
|
<!-- Kalendarz - główna pozycja -->
|
|
<li><a href="{{ url_for('calendar.calendar_index') }}" class="nav-link {% if request.endpoint and 'calendar' in request.endpoint %}active{% endif %}">Kalendarz</a></li>
|
|
|
|
<!-- B2B (Tablica) - główna pozycja -->
|
|
<li><a href="{{ url_for('classifieds.classifieds_index') }}" class="nav-link {% if request.endpoint and 'classifieds' in request.endpoint %}active{% endif %}">B2B</a></li>
|
|
|
|
<!-- Forum - główna pozycja -->
|
|
<li><a href="{{ url_for('forum_index') }}" class="nav-link {% if request.endpoint and 'forum' in request.endpoint %}active{% endif %}">Forum</a></li>
|
|
|
|
<!-- Wiadomości - dla zalogowanych członków -->
|
|
{% if current_user.is_authenticated %}
|
|
<li>
|
|
<a href="{{ url_for('messages.conversations_page') }}" class="nav-link {% if request.endpoint and 'messages' in (request.endpoint or '') %}active{% endif %}" style="position: relative;">
|
|
Wiadomości
|
|
<span class="nav-unread-badge" id="navMessagesBadge" style="display: none; position: absolute; top: -2px; right: -10px; background: #ef4444; color: white; font-size: 10px; font-weight: 700; min-width: 16px; height: 16px; border-radius: 8px; display: none; align-items: center; justify-content: center; padding: 0 4px;">0</span>
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
<!-- Aktualności - główna pozycja -->
|
|
<li><a href="{{ url_for('announcements_list') }}" class="nav-link {% if request.endpoint and 'announcement' in request.endpoint %}active{% endif %}">Aktualności</a></li>
|
|
|
|
<li><a href="{{ url_for('education.education_index') }}" class="nav-link {% if request.endpoint and 'education' in request.endpoint %}active{% endif %}">Edukacja</a></li>
|
|
|
|
<!-- Rada Izby - tylko dla członków Rady -->
|
|
{% if current_user.is_authenticated and (current_user.is_rada_member or current_user.can_access_admin_panel()) %}
|
|
<li><a href="{{ url_for('board.index') }}" class="nav-link {% if request.endpoint and 'board' in request.endpoint %}active{% endif %}">Rada</a></li>
|
|
{% endif %}
|
|
|
|
<!-- Projekty dropdown — for all logged-in users -->
|
|
<li class="nav-dropdown">
|
|
<a href="#" class="nav-link {% if request.endpoint and ('zopk' in (request.endpoint or '') or 'pej' in (request.endpoint or '')) %}active{% endif %}">Kaszubia/PEJ</a>
|
|
<ul class="nav-dropdown-menu">
|
|
<li><a href="{{ url_for('zopk_index') }}">Kaszubia</a></li>
|
|
<li><a href="{{ url_for('pej_index') }}">PEJ <span class="nav-badge-beta">beta</span></a></li>
|
|
</ul>
|
|
</li>
|
|
|
|
<!-- Notifications -->
|
|
<li class="notifications-dropdown">
|
|
<button class="notifications-trigger nav-link-with-badge" onclick="toggleNotifications(event)" aria-label="Powiadomienia">
|
|
<svg class="notifications-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>
|
|
</svg>
|
|
{% if unread_notifications_count > 0 %}
|
|
<span class="nav-badge" id="notificationBadge">{{ unread_notifications_count if unread_notifications_count <= 99 else '99+' }}</span>
|
|
{% else %}
|
|
<span class="nav-badge" id="notificationBadge" style="display: none;">0</span>
|
|
{% endif %}
|
|
</button>
|
|
<div class="notifications-menu" id="notificationsMenu">
|
|
<div class="notifications-header">
|
|
<span>Powiadomienia</span>
|
|
<button class="notifications-mark-all" onclick="markAllNotificationsRead()">Oznacz wszystkie</button>
|
|
</div>
|
|
<div id="notificationsList">
|
|
<div class="notifications-empty">Ladowanie...</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
|
|
<!-- User Menu -->
|
|
<li class="user-dropdown">
|
|
<button class="user-trigger" onclick="toggleUserMenu(event)">
|
|
<span class="user-avatar">{{ current_user.name[:1].upper() }}</span>
|
|
<span class="user-name">{{ current_user.name.split()[0] }}</span>
|
|
<svg class="user-chevron" width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
</svg>
|
|
</button>
|
|
</li>
|
|
{% else %}
|
|
<li><a href="{{ url_for('login') }}" class="btn btn-outline btn-sm">Zaloguj</a></li>
|
|
<li><a href="{{ url_for('register') }}" class="btn btn-primary btn-sm">Rejestracja</a></li>
|
|
<li>
|
|
<a href="https://norda-biznes.info" target="_blank" class="nav-link" style="display: flex; align-items: center; gap: 6px;">
|
|
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
|
</svg>
|
|
Strefa Gościa
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
{% if current_user.is_authenticated %}
|
|
<!-- User menu (outside header for proper z-index stacking on iOS) -->
|
|
<div class="user-menu-overlay" id="userMenuOverlay" onclick="closeUserMenu()"></div>
|
|
<div class="user-menu" id="userMenu">
|
|
<div class="user-menu-handle"></div>
|
|
<div class="user-menu-mobile-header">
|
|
<span class="user-avatar">{{ current_user.name[:1].upper() }}</span>
|
|
<span class="user-menu-mobile-name">{{ current_user.name }}</span>
|
|
</div>
|
|
<a href="{{ url_for('dashboard') }}" class="user-menu-item">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
|
</svg>
|
|
Mój Panel
|
|
</a>
|
|
<a href="{{ url_for('messages.conversations_page') }}" class="user-menu-item user-menu-item-badge">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
</svg>
|
|
Wiadomości
|
|
<span class="user-menu-badge" id="userMenuUnreadBadge" style="display: none;">0</span>
|
|
</a>
|
|
<div class="user-menu-divider"></div>
|
|
<a href="{{ url_for('konto_dane') }}" class="user-menu-item">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
|
</svg>
|
|
Moje konto
|
|
</a>
|
|
{% if current_user.company_id and current_user.can_manage_company(current_user.company_id) %}
|
|
<a href="{{ url_for('public.company_edit', company_id=current_user.company_id) }}" class="user-menu-item">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
|
</svg>
|
|
Moja firma
|
|
</a>
|
|
{% elif current_user.company_id %}
|
|
<a href="{{ url_for('public.company_detail', company_id=current_user.company_id) }}" class="user-menu-item">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
|
</svg>
|
|
Moja firma
|
|
</a>
|
|
{% endif %}
|
|
{% if current_user.company_id and current_user.email == 'maciej.pienczyn@inpi.pl' %}
|
|
<a href="{{ url_for('auth.konto_integracje') }}" class="user-menu-item owner-only">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
|
|
</svg>
|
|
Integracje
|
|
</a>
|
|
{% endif %}
|
|
{% if not current_user.company_id %}
|
|
<a href="{{ url_for('membership.apply') }}" class="user-menu-item" style="color: var(--primary);">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"/>
|
|
</svg>
|
|
Złóż deklarację
|
|
</a>
|
|
{% elif current_user.company and not current_user.company.nip %}
|
|
<a href="{{ url_for('membership.data_request') }}" class="user-menu-item" style="color: var(--warning);">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
</svg>
|
|
Uzupełnij dane firmy
|
|
</a>
|
|
{% endif %}
|
|
<a href="{{ url_for('release_notes') }}" class="user-menu-item">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
|
|
</svg>
|
|
Co nowego
|
|
</a>
|
|
<a href="{{ url_for('logout') }}" class="user-menu-item user-menu-logout">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
|
|
</svg>
|
|
Wyloguj
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
|
|
<!-- Admin Bar -->
|
|
<div class="admin-bar">
|
|
<div class="admin-bar-inner">
|
|
<div class="admin-bar-label">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
|
</svg>
|
|
Admin
|
|
</div>
|
|
|
|
<!-- Audyty (alphabetical: Audyt GBP, Audyt IT, Audyt KRS, Audyt SEO, Audyt Social, Kontrola dostępu, SEO Portalu) -->
|
|
<div class="admin-dropdown">
|
|
<button class="admin-dropdown-trigger" onclick="var p=this.parentElement,w=p.classList.contains('open');document.querySelectorAll('.admin-dropdown.open').forEach(function(d){d.classList.remove('open')});if(!w){p.classList.add('open');var m=p.querySelector('.admin-dropdown-menu');if(m&&window.innerWidth<=768){m.style.top=this.getBoundingClientRect().bottom+'px'}}event.stopPropagation()">
|
|
Audyty
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
</svg>
|
|
</button>
|
|
<div class="admin-dropdown-menu">
|
|
{% if is_audit_owner %}
|
|
<a href="{{ url_for('admin.admin_gbp_audit') }}" class="owner-only">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
</svg>
|
|
Audyt GBP
|
|
</a>
|
|
<a href="{{ url_for('admin.admin_it_audit') }}" class="owner-only">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
</svg>
|
|
Audyt IT
|
|
</a>
|
|
{% endif %}
|
|
<a href="{{ url_for('admin.admin_krs_audit') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
</svg>
|
|
Audyt KRS
|
|
</a>
|
|
{% if is_audit_owner %}
|
|
<a href="{{ url_for('admin.admin_seo') }}" class="owner-only">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
</svg>
|
|
Audyt SEO
|
|
</a>
|
|
<a href="{{ url_for('admin.admin_social_audit') }}" class="owner-only">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"/>
|
|
</svg>
|
|
Audyt Social
|
|
</a>
|
|
<a href="{{ url_for('admin.admin_access_overview') }}" class="owner-only">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
|
</svg>
|
|
Kontrola dostępu
|
|
</a>
|
|
<a href="{{ url_for('admin.admin_portal_seo') }}" class="owner-only">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
</svg>
|
|
SEO Portalu
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Izba (alphabetical: Deklaracje, Firmy, Jakość danych, Korzyści, Składki) -->
|
|
<div class="admin-dropdown">
|
|
<button class="admin-dropdown-trigger" onclick="var p=this.parentElement,w=p.classList.contains('open');document.querySelectorAll('.admin-dropdown.open').forEach(function(d){d.classList.remove('open')});if(!w){p.classList.add('open');var m=p.querySelector('.admin-dropdown-menu');if(m&&window.innerWidth<=768){m.style.top=this.getBoundingClientRect().bottom+'px'}}event.stopPropagation()">
|
|
Izba
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
</svg>
|
|
</button>
|
|
<div class="admin-dropdown-menu">
|
|
<a href="{{ url_for('admin.admin_membership') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"/>
|
|
</svg>
|
|
Deklaracje
|
|
</a>
|
|
<a href="{{ url_for('admin.admin_companies') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
|
|
</svg>
|
|
Firmy
|
|
</a>
|
|
{% if current_user.has_role(SystemRole.ADMIN) %}
|
|
<a href="{{ url_for('admin.admin_benefits') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"/>
|
|
</svg>
|
|
Korzyści
|
|
</a>
|
|
{% endif %}
|
|
<a href="{{ url_for('admin.admin_fees') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
Składki
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Social media (alphabetical: Audyt social media, Insights AI, Publikacja social media) -->
|
|
<div class="admin-dropdown">
|
|
<button class="admin-dropdown-trigger" onclick="var p=this.parentElement,w=p.classList.contains('open');document.querySelectorAll('.admin-dropdown.open').forEach(function(d){d.classList.remove('open')});if(!w){p.classList.add('open');var m=p.querySelector('.admin-dropdown-menu');if(m&&window.innerWidth<=768){m.style.top=this.getBoundingClientRect().bottom+'px'}}event.stopPropagation()">
|
|
Social media
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
</svg>
|
|
</button>
|
|
<div class="admin-dropdown-menu">
|
|
<a href="{{ url_for('admin.admin_social_audit') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z"/>
|
|
</svg>
|
|
Audyt social media
|
|
</a>
|
|
<a href="{{ url_for('admin.admin_insights') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
|
</svg>
|
|
Insights AI
|
|
</a>
|
|
<a href="{{ url_for('admin.social_publisher_list') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
|
|
</svg>
|
|
Publikacja social media <span class="nav-badge-beta">beta</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System (alphabetical: Analityka, Bezpieczeństwo, Health Check, Monitoring AI, Status systemu, Użytkownicy) -->
|
|
<div class="admin-dropdown">
|
|
<button class="admin-dropdown-trigger" onclick="var p=this.parentElement,w=p.classList.contains('open');document.querySelectorAll('.admin-dropdown.open').forEach(function(d){d.classList.remove('open')});if(!w){p.classList.add('open');var m=p.querySelector('.admin-dropdown-menu');if(m&&window.innerWidth<=768){m.style.top=this.getBoundingClientRect().bottom+'px'}}event.stopPropagation()">
|
|
System
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
</svg>
|
|
</button>
|
|
<div class="admin-dropdown-menu">
|
|
<a href="{{ url_for('admin.user_insights') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
|
</svg>
|
|
Analityka
|
|
</a>
|
|
{% if current_user.has_role(SystemRole.ADMIN) %}
|
|
<a href="{{ url_for('admin.admin_security') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
|
</svg>
|
|
Bezpieczeństwo
|
|
</a>
|
|
{% endif %}
|
|
<a href="{{ url_for('admin.admin_health') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
Health Check
|
|
</a>
|
|
<a href="{{ url_for('admin.admin_ai_usage') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
</svg>
|
|
Monitoring AI
|
|
</a>
|
|
<a href="{{ url_for('admin.admin_uptime') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
Monitoring uptime
|
|
</a>
|
|
<a href="{{ url_for('admin.admin_status') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
|
</svg>
|
|
Status systemu
|
|
</a>
|
|
{% if current_user.has_role(SystemRole.ADMIN) %}
|
|
<a href="{{ url_for('admin.admin_users') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
|
|
</svg>
|
|
Użytkownicy
|
|
</a>
|
|
<a href="{{ url_for('admin.admin_user_activity') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
|
</svg>
|
|
Aktywność użytkowników
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Treści (alphabetical: Kalendarz, Moderacja forum, Moderacja ogłoszeń, Moderacja rekomendacji, ZOP Kaszubia) -->
|
|
<div class="admin-dropdown">
|
|
<button class="admin-dropdown-trigger" onclick="var p=this.parentElement,w=p.classList.contains('open');document.querySelectorAll('.admin-dropdown.open').forEach(function(d){d.classList.remove('open')});if(!w){p.classList.add('open');var m=p.querySelector('.admin-dropdown-menu');if(m&&window.innerWidth<=768){m.style.top=this.getBoundingClientRect().bottom+'px'}}event.stopPropagation()">
|
|
Treści
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
</svg>
|
|
</button>
|
|
<div class="admin-dropdown-menu">
|
|
<a href="{{ url_for('admin.admin_calendar') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
|
</svg>
|
|
Kalendarz
|
|
</a>
|
|
<a href="{{ url_for('admin_forum') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"/>
|
|
</svg>
|
|
Moderacja forum
|
|
</a>
|
|
<a href="{{ url_for('admin.admin_announcements') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z"/>
|
|
</svg>
|
|
Moderacja ogłoszeń
|
|
</a>
|
|
<a href="{{ url_for('admin.admin_recommendations') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
|
|
</svg>
|
|
Moderacja rekomendacji
|
|
</a>
|
|
<a href="{{ url_for('admin_zopk') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
ZOP Kaszubia
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Narzędzia (owner-only: Kontakty zewnętrzne, Raporty, Mapa Powiązań) -->
|
|
{% if is_audit_owner %}
|
|
<div class="admin-dropdown">
|
|
<button class="admin-dropdown-trigger" onclick="var p=this.parentElement,w=p.classList.contains('open');document.querySelectorAll('.admin-dropdown.open').forEach(function(d){d.classList.remove('open')});if(!w){p.classList.add('open');var m=p.querySelector('.admin-dropdown-menu');if(m&&window.innerWidth<=768){m.style.top=this.getBoundingClientRect().bottom+'px'}}event.stopPropagation()">
|
|
Narzędzia
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
</svg>
|
|
</button>
|
|
<div class="admin-dropdown-menu">
|
|
<a href="{{ url_for('contacts.contacts_list') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
|
</svg>
|
|
Kontakty zewnętrzne
|
|
</a>
|
|
<a href="{{ url_for('reports.reports_index') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
</svg>
|
|
Raporty
|
|
</a>
|
|
<a href="#" onclick="openConnectionsMap(); return false;">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l5.447 2.724A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/>
|
|
</svg>
|
|
Mapa Powiązań
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Development Notice Banner -->
|
|
<div class="dev-notice" id="devNotice">
|
|
<span class="dev-notice-icon">🎉</span>
|
|
<span class="dev-notice-text">
|
|
Oficjalne uruchomienie portalu: <strong>9 kwietnia 2026</strong> — prezentacja dla członków Izby w Urzędzie Miasta Wejherowo.
|
|
</span>
|
|
<button class="dev-notice-close" onclick="dismissDevNotice()" aria-label="Zamknij">×</button>
|
|
</div>
|
|
|
|
<!-- Flash Messages -->
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
<div class="flash-messages" role="alert" aria-live="polite">
|
|
{% for category, message in messages %}
|
|
<div class="flash flash-{{ category }}">
|
|
<span>{{ message }}</span>
|
|
<button class="flash-close" onclick="this.parentElement.remove()" aria-label="Close">×</button>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
<!-- Main Content -->
|
|
<main role="main">
|
|
<div class="container {% block container_class %}{% endblock %}">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Footer -->
|
|
<footer>
|
|
<div class="container">
|
|
<div class="footer-content">
|
|
<div class="footer-section">
|
|
<img src="{{ url_for('static', filename='img/norda-logo.svg') }}" alt="Norda Biznes" height="48" style="margin-bottom: 12px; display: block;">
|
|
<h3>Norda Biznes Partner</h3>
|
|
<p>Strefa partnera Stowarzyszenia Norda Biznes - networking i współpraca biznesowa.</p>
|
|
</div>
|
|
<div class="footer-section">
|
|
<h3>Linki</h3>
|
|
<a href="{{ url_for('index') }}">Katalog firm</a>
|
|
<a href="{{ url_for('search') }}">Wyszukiwarka</a>
|
|
{% if current_user.is_authenticated %}
|
|
<a href="{{ url_for('calendar.calendar_index') }}">Kalendarz</a>
|
|
<a href="{{ url_for('classifieds.classifieds_index') }}">Tablica B2B</a>
|
|
<a href="{{ url_for('chat') }}">NordaGPT</a>
|
|
<a href="{{ url_for('public.chamber_authorities') }}">Władze Izby</a>
|
|
{% endif %}
|
|
<a href="{{ url_for('public.pwa_install') }}" class="footer-pwa-link">📱 Zainstaluj aplikację</a>
|
|
<a href="{{ url_for('public.polityka_prywatnosci') }}">Polityka prywatności</a>
|
|
<a href="{{ url_for('public.regulamin') }}">Regulamin</a>
|
|
</div>
|
|
<div class="footer-section">
|
|
<h3>Kontakt</h3>
|
|
<p>Email: <a href="mailto:maciej.pienczyn@inpi.pl">maciej.pienczyn@inpi.pl</a> <span style="font-size: 0.85em; color: #9ca3af;">(docelowo: kontakt@nordabiznes.pl)</span></p>
|
|
<p>Telefon: <a href="tel:+48729716400">+48 729 716 400</a></p>
|
|
<p>WhatsApp: <a href="https://wa.me/48729716400" target="_blank" rel="noopener">+48 729 716 400</a></p>
|
|
<p>Facebook: <a href="https://www.facebook.com/profile.php?id=100057396041901" target="_blank" rel="noopener">Izba Norda Business</a></p>
|
|
<p>Adres: ul. 12 Marca 238/5, 84-200 Wejherowo</p>
|
|
</div>
|
|
</div>
|
|
<div class="footer-bottom">
|
|
<div class="footer-creator">
|
|
<span>Stworzone przez</span>
|
|
<a href="https://inpi.pl" target="_blank" rel="noopener" class="creator-link">
|
|
<img src="{{ url_for('static', filename='img/INPI_LOGO_white_background.png') }}" alt="INPI" class="creator-logo">
|
|
</a>
|
|
</div>
|
|
<p>© {{ current_year }} INPI. Wszelkie prawa zastrzeżone.</p>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- PWA Smart Banner (mobile-only) -->
|
|
<div class="pwa-smart-banner" id="pwaSmartBanner">
|
|
<div class="pwa-smart-banner-icon">
|
|
<img src="{{ url_for('static', filename='img/favicon-192.png') }}" alt="Norda Biznes">
|
|
</div>
|
|
<div class="pwa-smart-banner-text">
|
|
<strong>Norda Biznes Partner</strong>
|
|
<span>Dodaj do ekranu głównego</span>
|
|
</div>
|
|
<a href="{{ url_for('public.pwa_install') }}" class="pwa-smart-banner-action">Zainstaluj</a>
|
|
<button class="pwa-smart-banner-close" onclick="dismissPwaBanner()" aria-label="Zamknij">
|
|
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Scripts -->
|
|
<script>
|
|
// User menu toggle
|
|
function toggleUserMenu(event) {
|
|
event.stopPropagation();
|
|
var userDropdown = document.querySelector('.user-dropdown');
|
|
var userMenu = document.getElementById('userMenu');
|
|
var overlay = document.getElementById('userMenuOverlay');
|
|
|
|
// Close notifications if open
|
|
var notificationsMenu = document.getElementById('notificationsMenu');
|
|
if (notificationsMenu) notificationsMenu.classList.remove('show');
|
|
|
|
var isOpen = userMenu.classList.contains('show');
|
|
if (isOpen) {
|
|
closeUserMenu();
|
|
} else {
|
|
// On mobile: close hamburger first, then show bottom sheet
|
|
if (window.innerWidth <= 768) {
|
|
var navMenu = document.getElementById('navMenu');
|
|
if (navMenu) navMenu.classList.remove('active');
|
|
}
|
|
userDropdown.classList.add('active');
|
|
userMenu.classList.add('show');
|
|
if (overlay) overlay.classList.add('show');
|
|
}
|
|
}
|
|
|
|
function closeUserMenu() {
|
|
const userDropdown = document.querySelector('.user-dropdown');
|
|
const userMenu = document.getElementById('userMenu');
|
|
const overlay = document.getElementById('userMenuOverlay');
|
|
if (userDropdown) userDropdown.classList.remove('active');
|
|
if (userMenu) userMenu.classList.remove('show');
|
|
if (overlay) overlay.classList.remove('show');
|
|
}
|
|
|
|
// Close user menu when clicking outside (desktop)
|
|
document.addEventListener('click', function(event) {
|
|
const userDropdown = document.querySelector('.user-dropdown');
|
|
if (userDropdown && !userDropdown.contains(event.target)) {
|
|
closeUserMenu();
|
|
}
|
|
});
|
|
|
|
// Update unread badge in user menu
|
|
function updateUserMenuBadge(count) {
|
|
const badge = document.getElementById('userMenuUnreadBadge');
|
|
if (badge) {
|
|
if (count > 0) {
|
|
badge.textContent = count > 99 ? '99+' : count;
|
|
badge.style.display = 'inline';
|
|
} else {
|
|
badge.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mobile menu toggle
|
|
function toggleMobileMenu() {
|
|
const menu = document.getElementById('navMenu');
|
|
const isOpen = menu.classList.contains('active');
|
|
if (!isOpen) {
|
|
// Position menu just below the header using getBoundingClientRect
|
|
const header = document.querySelector('header');
|
|
if (header) {
|
|
menu.style.top = header.getBoundingClientRect().bottom + 'px';
|
|
}
|
|
}
|
|
menu.classList.toggle('active');
|
|
}
|
|
|
|
// Auto-dismiss flash messages after 5 seconds
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const flashes = document.querySelectorAll('.flash');
|
|
flashes.forEach(flash => {
|
|
setTimeout(() => {
|
|
flash.style.animation = 'slideOut 0.3s ease-out';
|
|
setTimeout(() => flash.remove(), 300);
|
|
}, 5000);
|
|
});
|
|
});
|
|
|
|
// Close mobile menu when clicking outside
|
|
document.addEventListener('click', function(event) {
|
|
const navMenu = document.getElementById('navMenu');
|
|
const navToggle = document.querySelector('.nav-toggle');
|
|
|
|
if (!navMenu.contains(event.target) && !navToggle.contains(event.target)) {
|
|
navMenu.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
// Development notice banner
|
|
function dismissDevNotice() {
|
|
const notice = document.getElementById('devNotice');
|
|
if (notice) {
|
|
notice.style.display = 'none';
|
|
// Remember dismissal for this session
|
|
sessionStorage.setItem('devNoticeDismissed', 'true');
|
|
}
|
|
}
|
|
|
|
// Check if notice was dismissed in this session
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
if (sessionStorage.getItem('devNoticeDismissed') === 'true') {
|
|
const notice = document.getElementById('devNotice');
|
|
if (notice) notice.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Fetch unread message count (for authenticated users)
|
|
{% if current_user.is_authenticated %}
|
|
async function updateUnreadBadge() {
|
|
try {
|
|
const response = await fetch('{{ url_for("api_unread_count") }}');
|
|
const data = await response.json();
|
|
const count = data.count || 0;
|
|
const displayCount = count > 99 ? '99+' : count;
|
|
|
|
// Update all message badges
|
|
['unreadBadge', 'navMessagesBadge', 'userMenuUnreadBadge'].forEach(id => {
|
|
const badge = document.getElementById(id);
|
|
if (badge) {
|
|
if (count > 0) {
|
|
badge.textContent = displayCount;
|
|
badge.style.display = id === 'userMenuUnreadBadge' ? 'inline' : 'flex';
|
|
} else {
|
|
badge.style.display = 'none';
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.log('Could not fetch unread count');
|
|
}
|
|
}
|
|
// Check unread on page load and every 60 seconds
|
|
document.addEventListener('DOMContentLoaded', updateUnreadBadge);
|
|
setInterval(updateUnreadBadge, 60000);
|
|
|
|
// ============================================================
|
|
// NOTIFICATIONS SYSTEM
|
|
// ============================================================
|
|
|
|
let notificationsLoaded = false;
|
|
|
|
function toggleNotifications(event) {
|
|
event.stopPropagation();
|
|
const menu = document.getElementById('notificationsMenu');
|
|
const isOpen = menu.classList.contains('show');
|
|
|
|
// Close any other open dropdowns
|
|
document.querySelectorAll('.notifications-menu.show').forEach(m => m.classList.remove('show'));
|
|
|
|
if (!isOpen) {
|
|
menu.classList.add('show');
|
|
if (!notificationsLoaded) {
|
|
loadNotifications();
|
|
}
|
|
}
|
|
}
|
|
|
|
async function loadNotifications() {
|
|
const listEl = document.getElementById('notificationsList');
|
|
try {
|
|
const response = await fetch('{{ url_for("api_notifications") }}?limit=10&unread_only=true');
|
|
const data = await response.json();
|
|
|
|
if (!data.success) {
|
|
listEl.innerHTML = '<div class="notifications-empty">Blad ladowania</div>';
|
|
return;
|
|
}
|
|
|
|
if (data.notifications.length === 0) {
|
|
listEl.innerHTML = '<div class="notifications-empty">Brak powiadomien</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
data.notifications.forEach(n => {
|
|
const timeAgo = formatTimeAgo(new Date(n.created_at));
|
|
const unreadClass = n.is_read ? '' : 'unread';
|
|
const dotHtml = n.is_read ? '' : '<span class="notification-dot"></span>';
|
|
const icon = getNotificationIcon(n.notification_type);
|
|
|
|
html += `
|
|
<a href="${n.action_url || '#'}" class="notification-item ${unreadClass}"
|
|
onclick="markNotificationRead(event, ${n.id})" data-id="${n.id}">
|
|
<div class="notification-title">${dotHtml}${icon}${escapeHtml(n.title)}</div>
|
|
<div class="notification-message">${escapeHtml(n.message || '')}</div>
|
|
<div class="notification-time">${timeAgo}</div>
|
|
</a>
|
|
`;
|
|
});
|
|
|
|
listEl.innerHTML = html;
|
|
notificationsLoaded = true;
|
|
|
|
// Update badge
|
|
updateNotificationBadge(data.unread_count);
|
|
|
|
} catch (error) {
|
|
console.error('Error loading notifications:', error);
|
|
listEl.innerHTML = '<div class="notifications-empty">Blad ladowania</div>';
|
|
}
|
|
}
|
|
|
|
function getNotificationIcon(type) {
|
|
const icons = {
|
|
'news': '<svg class="notification-type-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"></path></svg>',
|
|
'event': '<svg class="notification-type-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>',
|
|
'message': '<svg class="notification-type-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path></svg>',
|
|
'system': '<svg class="notification-type-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>',
|
|
'alert': '<svg class="notification-type-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>'
|
|
};
|
|
return icons[type] || icons['system'];
|
|
}
|
|
|
|
async function markNotificationRead(event, notificationId) {
|
|
// Don't prevent default - we want to navigate
|
|
try {
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content ||
|
|
document.querySelector('input[name="csrf_token"]')?.value || '';
|
|
|
|
await fetch(`/api/notifications/${notificationId}/read`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrfToken
|
|
}
|
|
});
|
|
|
|
// Remove item from list (only show unread)
|
|
const item = document.querySelector(`.notification-item[data-id="${notificationId}"]`);
|
|
if (item) {
|
|
item.style.animation = 'fadeOut 0.3s ease forwards';
|
|
setTimeout(() => {
|
|
item.remove();
|
|
// Check if list is empty
|
|
const listEl = document.getElementById('notificationsList');
|
|
if (listEl && !listEl.querySelector('.notification-item')) {
|
|
listEl.innerHTML = '<div class="notifications-empty">Brak powiadomien</div>';
|
|
}
|
|
}, 300);
|
|
}
|
|
|
|
// Refresh badge count
|
|
updateNotificationBadgeFromAPI();
|
|
} catch (error) {
|
|
console.error('Error marking notification as read:', error);
|
|
}
|
|
}
|
|
|
|
async function markAllNotificationsRead() {
|
|
try {
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content ||
|
|
document.querySelector('input[name="csrf_token"]')?.value || '';
|
|
|
|
const response = await fetch('/api/notifications/read-all', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrfToken
|
|
}
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
// Remove all items from list (only show unread)
|
|
const listEl = document.getElementById('notificationsList');
|
|
const items = listEl.querySelectorAll('.notification-item');
|
|
items.forEach((item, index) => {
|
|
item.style.animation = 'fadeOut 0.3s ease forwards';
|
|
item.style.animationDelay = `${index * 0.05}s`;
|
|
});
|
|
setTimeout(() => {
|
|
listEl.innerHTML = '<div class="notifications-empty">Brak powiadomien</div>';
|
|
}, 300 + items.length * 50);
|
|
|
|
// Update badge
|
|
updateNotificationBadge(0);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error marking all notifications as read:', error);
|
|
}
|
|
}
|
|
|
|
async function updateNotificationBadgeFromAPI() {
|
|
try {
|
|
const response = await fetch('/api/notifications/unread-count');
|
|
const data = await response.json();
|
|
updateNotificationBadge(data.count);
|
|
} catch (error) {
|
|
console.error('Error fetching notification count:', error);
|
|
}
|
|
}
|
|
|
|
function updateNotificationBadge(count) {
|
|
const badge = document.getElementById('notificationBadge');
|
|
if (badge) {
|
|
if (count > 0) {
|
|
badge.textContent = count > 99 ? '99+' : count;
|
|
badge.style.display = 'flex';
|
|
} else {
|
|
badge.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
function formatTimeAgo(date) {
|
|
const now = new Date();
|
|
const diff = now - date;
|
|
const seconds = Math.floor(diff / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const hours = Math.floor(minutes / 60);
|
|
const days = Math.floor(hours / 24);
|
|
|
|
if (days > 7) {
|
|
return date.toLocaleDateString('pl-PL');
|
|
} else if (days > 0) {
|
|
return `${days} ${days === 1 ? 'dzien' : 'dni'} temu`;
|
|
} else if (hours > 0) {
|
|
return `${hours} ${hours === 1 ? 'godzine' : 'godzin'} temu`;
|
|
} else if (minutes > 0) {
|
|
return `${minutes} min temu`;
|
|
} else {
|
|
return 'przed chwila';
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Close notifications menu when clicking outside
|
|
document.addEventListener('click', function(event) {
|
|
const menu = document.getElementById('notificationsMenu');
|
|
const dropdown = document.querySelector('.notifications-dropdown');
|
|
if (menu && dropdown && !dropdown.contains(event.target)) {
|
|
menu.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
// Check notification count periodically
|
|
setInterval(updateNotificationBadgeFromAPI, 60000);
|
|
{% endif %}
|
|
|
|
// Register Service Worker for PWA installability
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.register('/sw.js').catch(function() {});
|
|
}
|
|
|
|
// PWA install prompt — capture beforeinstallprompt for Android
|
|
var deferredInstallPrompt = null;
|
|
|
|
window.addEventListener('beforeinstallprompt', function(e) {
|
|
e.preventDefault();
|
|
deferredInstallPrompt = e;
|
|
|
|
// On Android we can install directly — change banner button behavior
|
|
var actionBtn = document.querySelector('.pwa-smart-banner-action');
|
|
if (actionBtn) {
|
|
actionBtn.href = '#';
|
|
actionBtn.addEventListener('click', function(ev) {
|
|
ev.preventDefault();
|
|
triggerPwaInstall();
|
|
});
|
|
}
|
|
});
|
|
|
|
window.addEventListener('appinstalled', function() {
|
|
deferredInstallPrompt = null;
|
|
dismissPwaBanner();
|
|
showPwaInstalledToast();
|
|
});
|
|
|
|
function showPwaInstalledToast() {
|
|
// Remove any existing toast
|
|
var old = document.getElementById('pwaInstalledToast');
|
|
if (old) old.remove();
|
|
|
|
var toast = document.createElement('div');
|
|
toast.id = 'pwaInstalledToast';
|
|
toast.innerHTML =
|
|
'<div style="display:flex;align-items:center;gap:12px;">' +
|
|
'<img src="{{ url_for("static", filename="img/favicon-192.png") }}" alt="" style="width:48px;height:48px;border-radius:12px;">' +
|
|
'<div>' +
|
|
'<strong style="display:block;font-size:16px;margin-bottom:2px;">Gotowe!</strong>' +
|
|
'<span style="font-size:14px;color:#475569;">Ikona <b>Norda Biznes</b> jest teraz na Twoim ekranie głównym. Zamknij przeglądarkę i otwórz aplikację stamtąd.</span>' +
|
|
'</div>' +
|
|
'</div>';
|
|
toast.style.cssText = 'position:fixed;bottom:24px;left:16px;right:16px;background:#fff;border:2px solid #10b981;border-radius:16px;padding:16px;box-shadow:0 8px 24px rgba(0,0,0,0.15);z-index:10000;animation:pwaToastIn 0.4s ease;font-family:var(--font-family);';
|
|
|
|
// Add animation keyframes if not present
|
|
if (!document.getElementById('pwaToastStyle')) {
|
|
var style = document.createElement('style');
|
|
style.id = 'pwaToastStyle';
|
|
style.textContent = '@keyframes pwaToastIn{from{transform:translateY(100px);opacity:0}to{transform:translateY(0);opacity:1}}';
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
// Auto-hide after 10 seconds
|
|
setTimeout(function() {
|
|
if (toast.parentNode) {
|
|
toast.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
|
|
toast.style.opacity = '0';
|
|
toast.style.transform = 'translateY(20px)';
|
|
setTimeout(function() { toast.remove(); }, 400);
|
|
}
|
|
}, 10000);
|
|
}
|
|
|
|
function triggerPwaInstall() {
|
|
if (deferredInstallPrompt) {
|
|
deferredInstallPrompt.prompt();
|
|
deferredInstallPrompt.userChoice.then(function(result) {
|
|
if (result.outcome === 'accepted') {
|
|
dismissPwaBanner();
|
|
}
|
|
deferredInstallPrompt = null;
|
|
});
|
|
} else {
|
|
// Fallback — go to instructions page (iOS or prompt unavailable)
|
|
window.location.href = '{{ url_for("public.pwa_install") }}';
|
|
}
|
|
}
|
|
|
|
// PWA detection — set cookie for backend analytics
|
|
(function() {
|
|
var modes = ['standalone', 'minimal-ui', 'fullscreen', 'browser'];
|
|
var detected = 'unknown';
|
|
for (var i = 0; i < modes.length; i++) {
|
|
if (window.matchMedia('(display-mode: ' + modes[i] + ')').matches) {
|
|
detected = modes[i];
|
|
break;
|
|
}
|
|
}
|
|
if (window.navigator.standalone === true) detected = 'ios-standalone';
|
|
if (detected !== 'browser' && detected !== 'unknown') {
|
|
document.cookie = 'pwa_mode=1; path=/; max-age=86400; SameSite=Lax';
|
|
}
|
|
})();
|
|
|
|
// PWA Smart Banner logic
|
|
(function() {
|
|
var banner = document.getElementById('pwaSmartBanner');
|
|
if (!banner) return;
|
|
|
|
// Don't show if: already dismissed, or running in standalone mode
|
|
var isStandalone = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone;
|
|
var isDismissed = localStorage.getItem('pwa_banner_dismissed');
|
|
|
|
if (isStandalone || isDismissed) return;
|
|
|
|
// Show after 3 seconds
|
|
setTimeout(function() {
|
|
banner.classList.add('visible');
|
|
}, 3000);
|
|
})();
|
|
|
|
function dismissPwaBanner() {
|
|
var banner = document.getElementById('pwaSmartBanner');
|
|
if (banner) {
|
|
banner.classList.remove('visible');
|
|
localStorage.setItem('pwa_banner_dismissed', '1');
|
|
}
|
|
}
|
|
|
|
// Close admin dropdowns on outside tap
|
|
document.addEventListener('click', function(e) {
|
|
if (!e.target.closest('.admin-dropdown')) {
|
|
document.querySelectorAll('.admin-dropdown.open').forEach(function(d) { d.classList.remove('open'); });
|
|
}
|
|
});
|
|
|
|
{% block extra_js %}{% endblock %}
|
|
</script>
|
|
|
|
<!-- D3.js for Connections Map Modal -->
|
|
<script src="{{ url_for('static', filename='js/vendor/d3.v7.min.js') }}"></script>
|
|
|
|
<!-- Scroll Animations (Sprint 4) -->
|
|
<script src="{{ url_for('static', filename='js/scroll-animations.js') }}" defer></script>
|
|
|
|
<!-- Connections Map Modal -->
|
|
{% include 'connections_modal.html' %}
|
|
|
|
{% if is_staging %}
|
|
<!-- Staging Test Panel -->
|
|
<button type="button" class="staging-panel-toggle" onclick="toggleStagingPanel()" title="Panel testowy">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
|
|
</svg>
|
|
</button>
|
|
<div class="staging-panel" id="stagingPanel">
|
|
<div class="staging-panel-header">
|
|
<span>Panel testowy</span>
|
|
<button type="button" class="staging-panel-close" onclick="toggleStagingPanel()" title="Zamknij panel">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" width="16" height="16">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="staging-panel-body">
|
|
{% for key, feature in staging_features.items() %}
|
|
<div class="staging-feature-item">
|
|
<div class="staging-feature-name">
|
|
<span class="staging-feature-status"></span>
|
|
{{ feature.name }}
|
|
</div>
|
|
<div class="staging-feature-desc">{{ feature.description }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<script>
|
|
function toggleStagingPanel() {
|
|
document.getElementById('stagingPanel').classList.toggle('open');
|
|
}
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
var panel = document.getElementById('stagingPanel');
|
|
if (panel) panel.classList.remove('open');
|
|
}
|
|
});
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var testNavItems = {{ staging_features.values()|map(attribute='nav_item')|select('defined')|list|tojson }};
|
|
document.querySelectorAll('.nav-menu .nav-link, .admin-bar a').forEach(function(link) {
|
|
var text = link.textContent.trim();
|
|
if (testNavItems.indexOf(text) !== -1) {
|
|
var badge = document.createElement('span');
|
|
badge.className = 'nav-test-badge';
|
|
badge.textContent = 'TEST';
|
|
link.appendChild(badge);
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
{% endif %}
|
|
|
|
<!-- Global Confirm Modal -->
|
|
<div id="nordaConfirmOverlay" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:1050; align-items:center; justify-content:center;">
|
|
<div style="max-width:420px; width:90%; background:var(--surface, #fff); border-radius:12px; padding:28px; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
|
|
<div style="text-align:center; margin-bottom:20px;">
|
|
<div id="nordaConfirmIcon" style="font-size:2.5em; margin-bottom:12px;">⚠️</div>
|
|
<h3 id="nordaConfirmTitle" style="margin:0 0 8px 0; font-size:18px;">Potwierdzenie</h3>
|
|
<p id="nordaConfirmMessage" style="color:var(--text-secondary, #6b7280); margin:0; font-size:14px; line-height:1.5;"></p>
|
|
</div>
|
|
<div style="display:flex; gap:10px; justify-content:center;">
|
|
<button type="button" id="nordaConfirmCancel" style="padding:10px 24px; border-radius:8px; border:1px solid var(--border-color, #d1d5db); background:white; color:var(--text-primary, #1f2937); font-weight:500; cursor:pointer; font-size:14px;">Anuluj</button>
|
|
<button type="button" id="nordaConfirmOk" style="padding:10px 24px; border-radius:8px; border:none; background:#dc2626; color:white; font-weight:600; cursor:pointer; font-size:14px;">Potwierdź</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
(function() {
|
|
var overlay = document.getElementById('nordaConfirmOverlay');
|
|
var pendingForm = null;
|
|
|
|
window.nordaConfirm = function(form, message, options) {
|
|
options = options || {};
|
|
pendingForm = form;
|
|
document.getElementById('nordaConfirmIcon').textContent = options.icon || '⚠️';
|
|
document.getElementById('nordaConfirmTitle').textContent = options.title || 'Potwierdzenie';
|
|
document.getElementById('nordaConfirmMessage').innerHTML = message;
|
|
var okBtn = document.getElementById('nordaConfirmOk');
|
|
okBtn.textContent = options.okText || 'Potwierdź';
|
|
okBtn.style.background = options.danger !== false ? '#dc2626' : 'var(--primary, #2E4872)';
|
|
overlay.style.display = 'flex';
|
|
return false;
|
|
};
|
|
|
|
document.getElementById('nordaConfirmOk').addEventListener('click', function() {
|
|
overlay.style.display = 'none';
|
|
if (pendingForm) { pendingForm.submit(); pendingForm = null; }
|
|
});
|
|
document.getElementById('nordaConfirmCancel').addEventListener('click', function() {
|
|
overlay.style.display = 'none';
|
|
pendingForm = null;
|
|
});
|
|
overlay.addEventListener('click', function(e) {
|
|
if (e.target === overlay) { overlay.style.display = 'none'; pendingForm = null; }
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|