feat: add "Pokaż więcej" button to ZOPK facts widget
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

- Add /api/zopk-facts endpoint returning paginated facts from distinct
  source articles, ordered by recency
- Add "Pokaż więcej" button with AJAX loading and fade-in animation
- New cards are clickable with same hover effect as initial ones

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-15 10:05:39 +01:00
parent 39fc5e0fac
commit 825d40e770
2 changed files with 96 additions and 1 deletions

View File

@ -9,7 +9,7 @@ connections map, release notes, dashboard.
import logging
from datetime import datetime, timedelta, date
from flask import render_template, request, redirect, url_for, flash, session, current_app, Response
from flask import render_template, request, redirect, url_for, flash, session, current_app, Response, jsonify
from flask_login import login_required, current_user
from sqlalchemy import or_, func
@ -182,6 +182,48 @@ def index():
db.close()
@bp.route('/api/zopk-facts')
@login_required
def api_zopk_facts():
"""API endpoint for loading more ZOPK facts (for homepage widget)."""
from database import ZOPKKnowledgeFact, ZOPKNews
offset = request.args.get('offset', 0, type=int)
db = SessionLocal()
try:
recent_news_ids = db.query(
ZOPKKnowledgeFact.source_news_id
).join(ZOPKNews).filter(
ZOPKKnowledgeFact.confidence_score >= 0.5,
ZOPKNews.published_at.isnot(None)
).group_by(
ZOPKKnowledgeFact.source_news_id, ZOPKNews.published_at
).order_by(ZOPKNews.published_at.desc()).offset(offset).limit(3).all()
news_ids = [r[0] for r in recent_news_ids]
facts = []
for nid in news_ids:
fact = db.query(ZOPKKnowledgeFact).join(ZOPKNews).filter(
ZOPKKnowledgeFact.source_news_id == nid
).first()
if fact:
type_labels = {'investment': 'inwestycja', 'decision': 'decyzja', 'event': 'wydarzenie',
'milestone': 'kamień milowy', 'statistic': 'dane', 'partnership': 'współpraca',
'project': 'projekt'}
facts.append({
'text': fact.full_text[:200],
'type': fact.fact_type,
'type_label': type_labels.get(fact.fact_type, 'fakt'),
'source_name': fact.source_news.source_name or fact.source_news.source_domain if fact.source_news else '',
'source_date': fact.source_news.published_at.strftime('%d.%m.%Y') if fact.source_news and fact.source_news.published_at else '',
'source_url': fact.source_news.url if fact.source_news else '',
})
return jsonify({'facts': facts, 'has_more': len(facts) == 3})
except Exception:
db.rollback()
return jsonify({'facts': [], 'has_more': False})
finally:
db.close()
@bp.route('/company/<int:company_id>')
def company_detail(company_id):
"""Company detail page - requires login and NORDA membership"""

View File

@ -1085,6 +1085,12 @@
</a>
{% endfor %}
</div>
<div style="text-align: center; margin-top: var(--spacing-md);">
<button id="zopkMoreBtn" onclick="loadMoreFacts()" style="background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3); color: white; padding: 8px 20px; border-radius: var(--radius); cursor: pointer; font-size: var(--font-size-sm); transition: background 0.2s;"
onmouseover="this.style.background='rgba(255,255,255,0.25)';" onmouseout="this.style.background='rgba(255,255,255,0.15)';">
Pokaż więcej
</button>
</div>
</div>
{% endif %}
@ -1686,4 +1692,51 @@
minimizeNordaGPT();
}
});
// ZOPK "Czy wiesz, że..." — load more facts
let zopkOffset = 3;
async function loadMoreFacts() {
const btn = document.getElementById('zopkMoreBtn');
btn.textContent = 'Ładowanie...';
btn.disabled = true;
try {
const resp = await fetch('/api/zopk-facts?offset=' + zopkOffset);
const data = await resp.json();
if (data.facts && data.facts.length > 0) {
const grid = btn.closest('div[data-animate]').querySelector('[style*="display: grid"]');
const typeColors = {investment:'rgba(16,185,129,0.3)',event:'rgba(59,130,246,0.3)',decision:'rgba(245,158,11,0.3)',milestone:'rgba(139,92,246,0.3)'};
data.facts.forEach(f => {
const card = document.createElement('a');
card.href = f.source_url || '/zopk';
card.target = '_blank';
card.rel = 'noopener';
card.style.cssText = 'background:rgba(255,255,255,0.12);border-radius:var(--radius);padding:var(--spacing-md);text-decoration:none;color:white;display:block;transition:background 0.2s,transform 0.2s;cursor:pointer;opacity:0;';
card.onmouseover = function(){this.style.background='rgba(255,255,255,0.22)';this.style.transform='translateY(-2px)';};
card.onmouseout = function(){this.style.background='rgba(255,255,255,0.12)';this.style.transform='none';};
const color = typeColors[f.type] || 'rgba(255,255,255,0.2)';
card.innerHTML = '<span style="display:inline-block;padding:1px 8px;border-radius:4px;font-size:11px;font-weight:600;margin-bottom:var(--spacing-xs);background:'+color+';">'+f.type_label+'</span>'
+ '<p style="font-size:var(--font-size-sm);line-height:1.5;margin:0;opacity:0.95;">'+f.text+(f.text.length>=200?'...':'')+'</p>'
+ '<div style="font-size:11px;margin-top:var(--spacing-xs);opacity:0.7;">'+f.source_name+' &bull; '+f.source_date+'<span style="float:right;opacity:0.8;">Czytaj &rarr;</span></div>';
grid.appendChild(card);
requestAnimationFrame(() => { card.style.transition = 'opacity 0.4s'; card.style.opacity = '1'; });
});
zopkOffset += data.facts.length;
if (!data.has_more) {
btn.textContent = 'To wszystko';
btn.disabled = true;
btn.style.opacity = '0.5';
} else {
btn.textContent = 'Pokaż więcej';
btn.disabled = false;
}
} else {
btn.textContent = 'Brak więcej faktów';
btn.disabled = true;
btn.style.opacity = '0.5';
}
} catch(e) {
btn.textContent = 'Pokaż więcej';
btn.disabled = false;
}
}
{% endblock %}