- Add database models for ZOPK projects, stakeholders, news, resources - Add migration with initial data (5 projects, 7 stakeholders) - Implement admin dashboard with news moderation workflow - Add Brave Search API integration for automated news discovery - Create public knowledge base pages (index, project detail, news list) - Add navigation links in main menu and admin bar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
490 lines
16 KiB
HTML
490 lines
16 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Zielony Okręg Przemysłowy Kaszubia - Norda Biznes Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.zopk-hero {
|
|
background: linear-gradient(135deg, #059669 0%, #047857 50%, #065f46 100%);
|
|
color: white;
|
|
padding: var(--spacing-2xl) 0;
|
|
margin-bottom: var(--spacing-xl);
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
|
|
.zopk-hero h1 {
|
|
font-size: var(--font-size-3xl);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.zopk-hero p {
|
|
font-size: var(--font-size-lg);
|
|
opacity: 0.9;
|
|
max-width: 800px;
|
|
}
|
|
|
|
.zopk-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
margin-top: var(--spacing-xl);
|
|
}
|
|
|
|
.zopk-stat {
|
|
text-align: center;
|
|
padding: var(--spacing-md);
|
|
background: rgba(255,255,255,0.1);
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.zopk-stat-value {
|
|
font-size: var(--font-size-2xl);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.zopk-stat-label {
|
|
font-size: var(--font-size-sm);
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.section-header h2 {
|
|
font-size: var(--font-size-xl);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* Projects Grid */
|
|
.projects-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-2xl);
|
|
}
|
|
|
|
.project-card {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-lg);
|
|
box-shadow: var(--shadow);
|
|
transition: var(--transition);
|
|
text-decoration: none;
|
|
color: inherit;
|
|
display: block;
|
|
border-left: 4px solid var(--primary);
|
|
}
|
|
|
|
.project-card:hover {
|
|
box-shadow: var(--shadow-md);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.project-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: var(--radius);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 24px;
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.project-card h3 {
|
|
font-size: var(--font-size-lg);
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.project-card p {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.project-status {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
margin-top: var(--spacing-sm);
|
|
}
|
|
|
|
.status-planned {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.status-in_progress {
|
|
background: #dbeafe;
|
|
color: #1e40af;
|
|
}
|
|
|
|
.status-completed {
|
|
background: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
/* Stakeholders */
|
|
.stakeholders-list {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-2xl);
|
|
}
|
|
|
|
.stakeholder-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
background: var(--surface);
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.stakeholder-avatar {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: var(--font-size-lg);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.stakeholder-info h4 {
|
|
font-size: var(--font-size-base);
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.stakeholder-info p {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.stakeholder-category {
|
|
font-size: var(--font-size-xs);
|
|
padding: 2px 6px;
|
|
border-radius: var(--radius-sm);
|
|
background: #f3f4f6;
|
|
color: #374151;
|
|
}
|
|
|
|
.category-government { background: #fee2e2; color: #991b1b; }
|
|
.category-local_authority { background: #dbeafe; color: #1e40af; }
|
|
.category-business { background: #dcfce7; color: #166534; }
|
|
|
|
/* News */
|
|
.news-grid {
|
|
display: grid;
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-2xl);
|
|
}
|
|
|
|
.news-card {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
gap: var(--spacing-md);
|
|
background: var(--surface);
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
text-decoration: none;
|
|
color: inherit;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.news-card:hover {
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.news-date {
|
|
text-align: center;
|
|
padding: var(--spacing-sm);
|
|
background: var(--background);
|
|
border-radius: var(--radius-sm);
|
|
min-width: 60px;
|
|
}
|
|
|
|
.news-date-day {
|
|
font-size: var(--font-size-xl);
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.news-date-month {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.news-content h4 {
|
|
font-size: var(--font-size-base);
|
|
margin-bottom: var(--spacing-xs);
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.news-content p {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.news-source {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-muted);
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
/* Resources */
|
|
.resources-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.resource-card {
|
|
background: var(--surface);
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
text-decoration: none;
|
|
color: inherit;
|
|
transition: var(--transition);
|
|
text-align: center;
|
|
}
|
|
|
|
.resource-card:hover {
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.resource-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
margin: 0 auto var(--spacing-sm);
|
|
background: var(--background);
|
|
border-radius: var(--radius);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.resource-card h4 {
|
|
font-size: var(--font-size-sm);
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.resource-type {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: var(--spacing-2xl);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.zopk-hero {
|
|
padding: var(--spacing-xl) var(--spacing-md);
|
|
}
|
|
|
|
.zopk-hero h1 {
|
|
font-size: var(--font-size-2xl);
|
|
}
|
|
|
|
.projects-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.news-card {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.news-date {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
justify-content: flex-start;
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="zopk-hero">
|
|
<div class="container">
|
|
<h1>Zielony Okręg Przemysłowy Kaszubia</h1>
|
|
<p>Strategiczna inicjatywa rozwoju regionu Pomorza - energetyka odnawialna, przemysł obronny, technologie przyszłości. Baza wiedzy o projektach transformacji gospodarczej Kaszub.</p>
|
|
|
|
<div class="zopk-stats">
|
|
<div class="zopk-stat">
|
|
<div class="zopk-stat-value">{{ stats.total_projects }}</div>
|
|
<div class="zopk-stat-label">Projekty</div>
|
|
</div>
|
|
<div class="zopk-stat">
|
|
<div class="zopk-stat-value">{{ stats.total_stakeholders }}</div>
|
|
<div class="zopk-stat-label">Interesariusze</div>
|
|
</div>
|
|
<div class="zopk-stat">
|
|
<div class="zopk-stat-value">{{ stats.total_news }}</div>
|
|
<div class="zopk-stat-label">Aktualności</div>
|
|
</div>
|
|
<div class="zopk-stat">
|
|
<div class="zopk-stat-value">{{ stats.total_resources }}</div>
|
|
<div class="zopk-stat-label">Materiały</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Projects Section -->
|
|
<section>
|
|
<div class="section-header">
|
|
<h2>Projekty strategiczne</h2>
|
|
</div>
|
|
|
|
{% if projects %}
|
|
<div class="projects-grid">
|
|
{% for project in projects %}
|
|
<a href="{{ url_for('zopk_project_detail', slug=project.slug) }}" class="project-card" style="border-left-color: {{ project.color or '#059669' }}">
|
|
<div class="project-icon" style="background: {{ project.color or '#059669' }}20; color: {{ project.color or '#059669' }}">
|
|
{% if project.icon == 'wind' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.7 7.7a2.5 2.5 0 1 1 1.8 4.3H2"/><path d="M9.6 4.6A2 2 0 1 1 11 8H2"/><path d="M12.6 19.4A2 2 0 1 0 14 16H2"/></svg>
|
|
{% elif project.icon == 'atom' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="1"/><path d="M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"/><path d="M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"/></svg>
|
|
{% elif project.icon == 'server' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="20" height="8" x="2" y="2" rx="2" ry="2"/><rect width="20" height="8" x="2" y="14" rx="2" ry="2"/><line x1="6" x2="6.01" y1="6" y2="6"/><line x1="6" x2="6.01" y1="18" y2="18"/></svg>
|
|
{% elif project.icon == 'flask' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 22h12l3-9H3l3 9Z"/><path d="M9 2v8l-3 5"/><path d="M15 2v8l3 5"/><line x1="9" x2="15" y1="2" y2="2"/></svg>
|
|
{% elif project.icon == 'shield' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
{% else %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
|
|
{% endif %}
|
|
</div>
|
|
<h3>{{ project.name }}</h3>
|
|
<p>{{ project.description[:150] }}{% if project.description|length > 150 %}...{% endif %}</p>
|
|
<span class="project-status status-{{ project.status }}">
|
|
{% if project.status == 'planned' %}Planowany{% elif project.status == 'in_progress' %}W realizacji{% else %}Zakończony{% endif %}
|
|
</span>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>Brak projektów do wyświetlenia.</p>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<!-- Stakeholders Section -->
|
|
<section>
|
|
<div class="section-header">
|
|
<h2>Kluczowi interesariusze</h2>
|
|
</div>
|
|
|
|
{% if stakeholders %}
|
|
<div class="stakeholders-list">
|
|
{% for stakeholder in stakeholders %}
|
|
<div class="stakeholder-card">
|
|
<div class="stakeholder-avatar">
|
|
{{ stakeholder.name[0].upper() }}
|
|
</div>
|
|
<div class="stakeholder-info">
|
|
<h4>{{ stakeholder.name }}</h4>
|
|
<p>{{ stakeholder.role or stakeholder.organization }}</p>
|
|
<span class="stakeholder-category category-{{ stakeholder.category }}">
|
|
{% if stakeholder.category == 'government' %}Rząd{% elif stakeholder.category == 'local_authority' %}Samorząd{% elif stakeholder.category == 'business' %}Biznes{% else %}{{ stakeholder.category }}{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>Brak interesariuszy do wyświetlenia.</p>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<!-- News Section -->
|
|
<section>
|
|
<div class="section-header">
|
|
<h2>Aktualności</h2>
|
|
<a href="{{ url_for('zopk_news_list') }}" class="btn btn-secondary btn-sm">Zobacz wszystkie</a>
|
|
</div>
|
|
|
|
{% if news_items %}
|
|
<div class="news-grid">
|
|
{% for news in news_items %}
|
|
<a href="{{ news.url }}" target="_blank" rel="noopener" class="news-card">
|
|
<div class="news-date">
|
|
{% if news.published_at %}
|
|
<div class="news-date-day">{{ news.published_at.strftime('%d') }}</div>
|
|
<div class="news-date-month">{{ news.published_at.strftime('%b') }}</div>
|
|
{% else %}
|
|
<div class="news-date-day">--</div>
|
|
<div class="news-date-month">---</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="news-content">
|
|
<h4>{{ news.title }}</h4>
|
|
{% if news.description %}
|
|
<p>{{ news.description[:200] }}{% if news.description|length > 200 %}...{% endif %}</p>
|
|
{% endif %}
|
|
<div class="news-source">{{ news.source_name or news.source_domain }}</div>
|
|
</div>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>Brak aktualności. Wkrótce pojawią się nowe informacje.</p>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<!-- Resources Section -->
|
|
{% if resources %}
|
|
<section>
|
|
<div class="section-header">
|
|
<h2>Materiały i dokumenty</h2>
|
|
</div>
|
|
|
|
<div class="resources-grid">
|
|
{% for resource in resources %}
|
|
<a href="{{ resource.url or '#' }}" target="_blank" rel="noopener" class="resource-card">
|
|
<div class="resource-icon">
|
|
{% if resource.resource_type == 'document' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
|
{% elif resource.resource_type == 'video' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect width="15" height="14" x="1" y="5" rx="2" ry="2"/></svg>
|
|
{% elif resource.resource_type == 'image' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>
|
|
{% elif resource.resource_type == 'map' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" x2="8" y1="2" y2="18"/><line x1="16" x2="16" y1="6" y2="22"/></svg>
|
|
{% else %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
|
{% endif %}
|
|
</div>
|
|
<h4>{{ resource.title }}</h4>
|
|
<div class="resource-type">{{ resource.resource_type|capitalize }}</div>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
{% endblock %}
|