feat: Add star ratings and sorting to ZOPK news management
- Add AI relevance score column with star display (1-5) - Add sortable column headers (title, score, date) - Add dropdown sort selector in filters - Preserve sort params in pagination links - Color-coded score badges based on relevance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e399022223
commit
33bb43caec
@ -174,11 +174,88 @@
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
/* AI Stars Rating */
|
||||
.ai-stars {
|
||||
display: inline-flex;
|
||||
gap: 1px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.ai-stars .star-filled { color: #f59e0b; }
|
||||
.ai-stars .star-empty { color: #d1d5db; }
|
||||
|
||||
.ai-score-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
.ai-score-badge.score-5 { background: #dcfce7; color: #166534; }
|
||||
.ai-score-badge.score-4 { background: #d1fae5; color: #047857; }
|
||||
.ai-score-badge.score-3 { background: #fef3c7; color: #92400e; }
|
||||
.ai-score-badge.score-2 { background: #fed7aa; color: #c2410c; }
|
||||
.ai-score-badge.score-1 { background: #fee2e2; color: #991b1b; }
|
||||
.ai-score-badge.score-none { background: #f3f4f6; color: #6b7280; }
|
||||
|
||||
/* Sortable Headers */
|
||||
.sortable-header {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sortable-header a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.sortable-header a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
.sortable-header.active a {
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
.sort-icon {
|
||||
font-size: 10px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.sortable-header.active .sort-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Sort Controls */
|
||||
.sort-controls {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
padding-left: var(--spacing-lg);
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
.sort-controls select {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.news-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.sort-controls {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
border-left: none;
|
||||
width: 100%;
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -194,21 +271,48 @@
|
||||
|
||||
<div class="filters">
|
||||
<span class="text-muted">Status:</span>
|
||||
<a href="{{ url_for('admin_zopk_news', status='all') }}" class="filter-btn {% if current_status == 'all' %}active{% endif %}">Wszystkie</a>
|
||||
<a href="{{ url_for('admin_zopk_news', status='pending') }}" class="filter-btn {% if current_status == 'pending' %}active{% endif %}">Oczekujące</a>
|
||||
<a href="{{ url_for('admin_zopk_news', status='approved') }}" class="filter-btn {% if current_status == 'approved' %}active{% endif %}">Zatwierdzone</a>
|
||||
<a href="{{ url_for('admin_zopk_news', status='rejected') }}" class="filter-btn {% if current_status == 'rejected' %}active{% endif %}">Odrzucone</a>
|
||||
<a href="{{ url_for('admin_zopk_news', status='all', sort=current_sort, dir=current_dir) }}" class="filter-btn {% if current_status == 'all' %}active{% endif %}">Wszystkie</a>
|
||||
<a href="{{ url_for('admin_zopk_news', status='pending', sort=current_sort, dir=current_dir) }}" class="filter-btn {% if current_status == 'pending' %}active{% endif %}">Oczekujące</a>
|
||||
<a href="{{ url_for('admin_zopk_news', status='approved', sort=current_sort, dir=current_dir) }}" class="filter-btn {% if current_status == 'approved' %}active{% endif %}">Zatwierdzone</a>
|
||||
<a href="{{ url_for('admin_zopk_news', status='rejected', sort=current_sort, dir=current_dir) }}" class="filter-btn {% if current_status == 'rejected' %}active{% endif %}">Odrzucone</a>
|
||||
|
||||
<div class="sort-controls">
|
||||
<span class="text-muted">Sortuj:</span>
|
||||
<select id="sort-select" onchange="updateSort()">
|
||||
<option value="date-desc" {% if current_sort == 'date' and current_dir == 'desc' %}selected{% endif %}>Data (najnowsze)</option>
|
||||
<option value="date-asc" {% if current_sort == 'date' and current_dir == 'asc' %}selected{% endif %}>Data (najstarsze)</option>
|
||||
<option value="score-desc" {% if current_sort == 'score' and current_dir == 'desc' %}selected{% endif %}>Ocena AI (najwyższa)</option>
|
||||
<option value="score-asc" {% if current_sort == 'score' and current_dir == 'asc' %}selected{% endif %}>Ocena AI (najniższa)</option>
|
||||
<option value="title-asc" {% if current_sort == 'title' and current_dir == 'asc' %}selected{% endif %}>Tytuł (A-Z)</option>
|
||||
<option value="title-desc" {% if current_sort == 'title' and current_dir == 'desc' %}selected{% endif %}>Tytuł (Z-A)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if news_items %}
|
||||
<table class="news-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40%">Tytuł</th>
|
||||
<th class="sortable-header {% if current_sort == 'title' %}active{% endif %}" style="width: 35%">
|
||||
<a href="{{ url_for('admin_zopk_news', status=current_status, sort='title', dir='desc' if current_sort == 'title' and current_dir == 'asc' else 'asc') }}">
|
||||
Tytuł
|
||||
<span class="sort-icon">{% if current_sort == 'title' %}{{ '▲' if current_dir == 'asc' else '▼' }}{% else %}⇅{% endif %}</span>
|
||||
</a>
|
||||
</th>
|
||||
<th>Źródło</th>
|
||||
<th>Typ</th>
|
||||
<th class="sortable-header {% if current_sort == 'score' %}active{% endif %}">
|
||||
<a href="{{ url_for('admin_zopk_news', status=current_status, sort='score', dir='asc' if current_sort == 'score' and current_dir == 'desc' else 'desc') }}">
|
||||
Ocena AI
|
||||
<span class="sort-icon">{% if current_sort == 'score' %}{{ '▲' if current_dir == 'asc' else '▼' }}{% else %}⇅{% endif %}</span>
|
||||
</a>
|
||||
</th>
|
||||
<th>Status</th>
|
||||
<th>Data</th>
|
||||
<th class="sortable-header {% if current_sort == 'date' %}active{% endif %}">
|
||||
<a href="{{ url_for('admin_zopk_news', status=current_status, sort='date', dir='asc' if current_sort == 'date' and current_dir == 'desc' else 'desc') }}">
|
||||
Data
|
||||
<span class="sort-icon">{% if current_sort == 'date' %}{{ '▲' if current_dir == 'asc' else '▼' }}{% else %}⇅{% endif %}</span>
|
||||
</a>
|
||||
</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -217,16 +321,32 @@
|
||||
<tr id="news-row-{{ news.id }}">
|
||||
<td class="news-title">
|
||||
<a href="{{ news.url }}" target="_blank" rel="noopener">{{ news.title }}</a>
|
||||
<small>{{ news.source_domain }}</small>
|
||||
<small>{{ news.source_name or news.source_domain }}</small>
|
||||
</td>
|
||||
<td>{{ news.source_name or news.source_domain }}</td>
|
||||
<td><span class="source-badge">{{ news.source_type }}</span></td>
|
||||
<td>
|
||||
{% if news.ai_relevance_score %}
|
||||
<span class="ai-score-badge score-{{ news.ai_relevance_score }}" title="{{ news.ai_evaluation_reason or 'Brak opisu' }}">
|
||||
<span class="ai-stars">
|
||||
{% for i in range(1, 6) %}
|
||||
<span class="{{ 'star-filled' if i <= news.ai_relevance_score else 'star-empty' }}">★</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
</span>
|
||||
{% elif news.ai_relevant is not none %}
|
||||
<span class="ai-score-badge score-{{ '3' if news.ai_relevant else '1' }}" title="{{ news.ai_evaluation_reason or 'Brak opisu' }}">
|
||||
{{ '✓ Relevant' if news.ai_relevant else '✗ Nie' }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="ai-score-badge score-none">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge status-{{ news.status }}">
|
||||
{% if news.status == 'pending' %}Oczekuje{% elif news.status == 'approved' %}Zatwierdzony{% else %}Odrzucony{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ news.created_at.strftime('%d.%m.%Y') if news.created_at else '-' }}</td>
|
||||
<td>{{ news.published_at.strftime('%d.%m.%Y') if news.published_at else (news.created_at.strftime('%d.%m.%Y') if news.created_at else '-') }}</td>
|
||||
<td>
|
||||
{% if news.status == 'pending' %}
|
||||
<button class="action-btn approve" onclick="approveNews({{ news.id }})">Zatwierdź</button>
|
||||
@ -245,21 +365,21 @@
|
||||
{% if total_pages > 1 %}
|
||||
<nav class="pagination">
|
||||
{% if page > 1 %}
|
||||
<a href="{{ url_for('admin_zopk_news', page=page-1, status=current_status) }}">« Poprzednia</a>
|
||||
<a href="{{ url_for('admin_zopk_news', page=page-1, status=current_status, sort=current_sort, dir=current_dir) }}">« Poprzednia</a>
|
||||
{% endif %}
|
||||
|
||||
{% for p in range(1, total_pages + 1) %}
|
||||
{% if p == page %}
|
||||
<span class="current">{{ p }}</span>
|
||||
{% elif p <= 3 or p > total_pages - 3 or (p >= page - 1 and p <= page + 1) %}
|
||||
<a href="{{ url_for('admin_zopk_news', page=p, status=current_status) }}">{{ p }}</a>
|
||||
<a href="{{ url_for('admin_zopk_news', page=p, status=current_status, sort=current_sort, dir=current_dir) }}">{{ p }}</a>
|
||||
{% elif p == 4 or p == total_pages - 3 %}
|
||||
<span>...</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page < total_pages %}
|
||||
<a href="{{ url_for('admin_zopk_news', page=page+1, status=current_status) }}">Następna »</a>
|
||||
<a href="{{ url_for('admin_zopk_news', page=page+1, status=current_status, sort=current_sort, dir=current_dir) }}">Następna »</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
@ -274,6 +394,16 @@
|
||||
{% block extra_js %}
|
||||
const csrfToken = '{{ csrf_token() }}';
|
||||
|
||||
function updateSort() {
|
||||
const select = document.getElementById('sort-select');
|
||||
const [sort, dir] = select.value.split('-');
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('sort', sort);
|
||||
url.searchParams.set('dir', dir);
|
||||
url.searchParams.delete('page'); // Reset to first page
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
async function approveNews(newsId) {
|
||||
try {
|
||||
const response = await fetch(`/admin/zopk/news/${newsId}/approve`, {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user