nordabiz/templates/classifieds/index.html
Maciej Pienczyn 2bf5c780e2
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
feat: Quill rich text editor in B2B classifieds + expiry email notifier
- Replace textarea with Quill editor in new/edit classified forms
- Sanitize HTML with sanitize_html() on save (XSS prevention)
- Render HTML in classified detail view, strip tags in list view
- New script: classified_expiry_notifier.py sends email 3 days before
  expiry with link to extend. Run daily via cron at 8:00.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:53:54 +02:00

315 lines
9.7 KiB
HTML
Executable File

{% extends "base.html" %}
{% block title %}Tablica B2B - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.classifieds-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xl);
}
.classifieds-header h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
}
.filters {
display: flex;
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
flex-wrap: wrap;
}
.filter-group {
display: flex;
gap: var(--spacing-xs);
}
.filter-btn {
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius);
border: 1px solid var(--border);
background: var(--surface);
text-decoration: none;
color: var(--text-secondary);
font-size: var(--font-size-sm);
transition: var(--transition);
}
.filter-btn:hover {
background: var(--background);
color: var(--text-primary);
}
.filter-btn.active {
background: var(--primary);
border-color: var(--primary);
color: white;
}
.classifieds-grid {
display: grid;
gap: var(--spacing-lg);
}
.classified-card {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow);
transition: var(--transition);
}
.classified-card:hover {
box-shadow: var(--shadow-md);
}
.classified-card.szukam {
border-left: 4px solid var(--warning);
}
.classified-card.oferuje {
border-left: 4px solid var(--success);
}
.classified-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-sm);
}
.classified-type {
display: inline-block;
padding: 2px 10px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 600;
text-transform: uppercase;
}
.classified-type.szukam {
background: #fef3c7;
color: #92400e;
}
.classified-type.oferuje {
background: #dcfce7;
color: #166534;
}
.classified-category {
display: inline-block;
padding: 2px 10px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 500;
text-transform: uppercase;
}
.category-uslugi {
background: #dbeafe;
color: #1e40af;
}
.category-produkty {
background: #fef3c7;
color: #92400e;
}
.category-wspolpraca {
background: #dcfce7;
color: #166534;
}
.category-praca {
background: #fce7f3;
color: #9d174d;
}
.category-inne {
background: #f3f4f6;
color: #374151;
}
.category-nieruchomosci {
background: #e0e7ff;
color: #3730a3;
}
/* Test item styling */
.classified-card.test-item {
opacity: 0.6;
border-right: 4px solid #9ca3af;
}
.test-badge {
display: inline-block;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 500;
background: #f3f4f6;
color: #6b7280;
margin-left: var(--spacing-xs);
}
.classified-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.classified-title a {
color: inherit;
text-decoration: none;
}
.classified-title a:hover {
color: var(--primary);
}
.classified-description {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-md);
line-height: 1.5;
}
.classified-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.classified-author {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.classified-stats {
display: flex;
gap: var(--spacing-md);
}
.empty-state {
text-align: center;
padding: var(--spacing-2xl);
color: var(--text-secondary);
background: var(--surface);
border-radius: var(--radius-lg);
}
.pagination {
display: flex;
justify-content: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-xl);
}
.pagination a {
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius);
text-decoration: none;
color: var(--text-secondary);
background: var(--surface);
border: 1px solid var(--border);
}
.pagination a:hover {
background: var(--background);
}
.pagination a.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
</style>
{% endblock %}
{% block content %}
<div class="classifieds-header">
<div>
<h1>Tablica B2B</h1>
<p class="text-muted">Ogłoszenia biznesowe członków Norda Biznes</p>
</div>
<a href="{{ url_for('classifieds.classifieds_new') }}" class="btn btn-primary">Dodaj ogłoszenie</a>
</div>
<div class="filters">
<div class="filter-group">
<a href="{{ url_for('classifieds.classifieds_index', category=category_filter) }}" class="filter-btn {% if not listing_type %}active{% endif %}">Wszystkie</a>
<a href="{{ url_for('classifieds.classifieds_index', type='szukam', category=category_filter) }}" class="filter-btn {% if listing_type == 'szukam' %}active{% endif %}">Szukam</a>
<a href="{{ url_for('classifieds.classifieds_index', type='oferuje', category=category_filter) }}" class="filter-btn {% if listing_type == 'oferuje' %}active{% endif %}">Oferuję</a>
</div>
<div class="filter-group">
<a href="{{ url_for('classifieds.classifieds_index', type=listing_type) }}" class="filter-btn {% if not category_filter %}active{% endif %}">Wszystkie</a>
{% for cat_value, cat_label in categories %}
<a href="{{ url_for('classifieds.classifieds_index', type=listing_type, category=cat_value) }}" class="filter-btn {% if category_filter == cat_value %}active{% endif %}">{{ cat_label }}</a>
{% endfor %}
</div>
</div>
<div class="classifieds-grid">
{% if classifieds %}
{% for classified in classifieds %}
<div class="classified-card {{ classified.listing_type }} {% if classified.is_test %}test-item{% endif %}">
<div class="classified-header">
<span class="classified-type {{ classified.listing_type }}">{{ 'Szukam' if classified.listing_type == 'szukam' else 'Oferuję' }}</span>
<span class="classified-category category-{{ classified.category }}">{{ classified.category|replace('uslugi', 'Usługi')|replace('produkty', 'Produkty')|replace('wspolpraca', 'Współpraca')|replace('praca', 'Praca')|replace('inne', 'Inne')|replace('nieruchomosci', 'Nieruchomości') }}</span>
{% if classified.is_test %}<span class="test-badge">Testowe</span>{% endif %}
</div>
<div class="classified-title">
<a href="{{ url_for('classifieds.classifieds_view', classified_id=classified.id) }}">{{ classified.title }}</a>
{% if classified.is_expired %}
<span style="display:inline-block; background:#fef2f2; color:#dc2626; font-size:11px; font-weight:600; padding:2px 8px; border-radius:4px; vertical-align:middle; margin-left:6px;">Wygasło</span>
{% elif not classified.is_active %}
<span style="display:inline-block; background:#f3f4f6; color:#6b7280; font-size:11px; font-weight:600; padding:2px 8px; border-radius:4px; vertical-align:middle; margin-left:6px;">Zamknięte</span>
{% endif %}
</div>
<div class="classified-description">
{{ classified.description|striptags|truncate(200) }}
</div>
<div class="classified-meta">
<div class="classified-author">
{% if classified.company %}
{{ classified.company.name }}
{% else %}
<a href="{{ url_for('public.user_profile', user_id=classified.author_id) }}" style="color:inherit;text-decoration:none;" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">{{ classified.author.name or classified.author.email.split('@')[0] }}</a>
{% endif %}
</div>
<div class="classified-stats">
<span>{{ classified.views_count }} wyswietl.</span>
<span>{{ classified.questions|length }} odpow.</span>
<span>{{ classified.created_at|local_time('%d.%m.%Y') }}</span>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<p>Brak ogloszen w tej kategorii</p>
<a href="{{ url_for('classifieds.classifieds_new') }}" class="btn btn-primary mt-2">Dodaj pierwsze ogłoszenie</a>
</div>
{% endif %}
</div>
{% if total_pages > 1 %}
<div class="pagination">
{% for p in range(1, total_pages + 1) %}
<a href="{{ url_for('classifieds.classifieds_index', type=listing_type, category=category_filter, page=p) }}" class="{% if p == page %}active{% endif %}">{{ p }}</a>
{% endfor %}
</div>
{% endif %}
{% endblock %}