nordabiz/templates/release_notes.html
Maciej Pienczyn c63bcb065b
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: add "Wypróbuj" deep links to release notes items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:26:26 +01:00

558 lines
17 KiB
HTML
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Historia zmian - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.page-header {
margin-bottom: var(--spacing-xl);
text-align: center;
}
.page-header h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.page-header p {
color: var(--text-secondary);
font-size: var(--font-size-lg);
}
.releases-container {
max-width: 800px;
margin: 0 auto;
}
.release-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-lg);
position: relative;
}
.release-card::before {
content: '';
position: absolute;
left: -20px;
top: 30px;
width: 12px;
height: 12px;
background: var(--primary);
border-radius: 50%;
border: 3px solid var(--surface);
box-shadow: 0 0 0 2px var(--primary);
}
.releases-timeline {
border-left: 2px solid var(--border);
padding-left: var(--spacing-xl);
margin-left: var(--spacing-sm);
}
.release-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-md);
flex-wrap: wrap;
gap: var(--spacing-sm);
}
.release-version {
font-size: var(--font-size-xl);
font-weight: 600;
color: var(--text-primary);
}
.release-date {
font-size: var(--font-size-sm);
color: var(--text-secondary);
background: var(--background);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius);
}
.release-badges {
display: flex;
gap: var(--spacing-xs);
flex-wrap: wrap;
}
.release-badge {
font-size: var(--font-size-xs);
padding: 2px 8px;
border-radius: var(--radius);
font-weight: 500;
text-transform: uppercase;
}
.badge-new {
background: #dcfce7;
color: #166534;
}
.badge-fix {
background: #fee2e2;
color: #991b1b;
}
.badge-improve {
background: #dbeafe;
color: #1e40af;
}
.badge-beta {
background: #fef3c7;
color: #92400e;
}
.release-changes {
margin-top: var(--spacing-md);
}
.change-category {
margin-bottom: var(--spacing-md);
}
.change-category h4 {
font-size: var(--font-size-sm);
color: var(--text-secondary);
text-transform: uppercase;
margin-bottom: var(--spacing-sm);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.change-category h4::before {
font-size: var(--font-size-base);
}
.change-category.new h4::before { content: '✨'; }
.change-category.fix h4::before { content: '🔧'; }
.change-category.improve h4::before { content: '⚡'; }
.change-category.security h4::before { content: '🔒'; }
.change-category.beta h4::before { content: '🧪'; }
.change-list {
list-style: none;
padding: 0;
margin: 0;
}
.change-list li {
padding: var(--spacing-xs) 0;
padding-left: var(--spacing-md);
position: relative;
color: var(--text-primary);
font-size: var(--font-size-sm);
}
.change-list li::before {
content: '•';
position: absolute;
left: 0;
color: var(--text-secondary);
}
.change-list li.release-highlight {
background: linear-gradient(90deg, rgba(251, 191, 36, 0.15) 0%, transparent 80%);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius);
margin-left: -4px;
margin-right: -4px;
}
.change-list li.release-highlight::before {
content: none;
}
.try-link {
color: var(--primary);
font-weight: 500;
text-decoration: none;
font-size: var(--font-size-xs);
margin-left: 6px;
white-space: nowrap;
}
.try-link:hover {
text-decoration: underline;
}
.stats-banner {
background: linear-gradient(135deg, var(--primary), var(--primary-dark, #1a56db));
color: white;
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: var(--spacing-lg);
text-align: center;
}
.notify-btn {
background: var(--primary);
color: white;
border: none;
padding: 6px 12px;
border-radius: var(--radius);
font-size: var(--font-size-xs);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
transition: background 0.2s;
}
.notify-btn:hover {
background: var(--primary-dark, #1a56db);
}
.notify-btn:disabled {
background: var(--text-secondary);
cursor: not-allowed;
}
.notify-btn.sent {
background: #16a34a;
}
.stat-item h3 {
font-size: var(--font-size-3xl);
font-weight: 700;
margin-bottom: var(--spacing-xs);
}
.stat-item p {
font-size: var(--font-size-sm);
opacity: 0.9;
}
@media (max-width: 768px) {
.releases-timeline {
padding-left: var(--spacing-md);
margin-left: 0;
}
.release-card::before {
left: -15px;
}
}
</style>
{% endblock %}
{% macro render_item(item, release) -%}
{%- set title = item|striptags|replace('★ ', '') -%}
{%- set title_short = title.split(' - ')[0]|trim -%}
{%- set link = release.get('links', {}).get(title_short, '') if release.get is defined else '' -%}
{{ item|safe }}{% if link %} <a href="{{ link }}" class="try-link">Wypróbuj &rarr;</a>{% endif %}
{%- endmacro %}
{% block content %}
<div class="container">
<div class="page-header">
<h1>📋 Historia zmian</h1>
<p>Dziennik rozwoju platformy Norda Biznes Partner</p>
</div>
<div class="stats-banner">
<div class="stat-item">
<h3>{{ stats.companies }}</h3>
<p>Firm w katalogu</p>
</div>
<div class="stat-item">
<h3>{{ stats.categories }}</h3>
<p>Kategorii branżowych</p>
</div>
<div class="stat-item">
<h3>{{ releases|length }}</h3>
<p>Aktualizacji</p>
</div>
<div class="stat-item">
<h3>2025</h3>
<p>Rok startu</p>
</div>
</div>
<div class="releases-container">
<div class="releases-timeline">
{% for release in releases %}
<div class="release-card">
<div class="release-header">
<div>
<div class="release-version">{{ release.version }}</div>
<div class="release-badges">
{% for badge in release.badges %}
<span class="release-badge badge-{{ badge }}">{{ badge }}</span>
{% endfor %}
</div>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<div class="release-date">{{ release.date }}</div>
{% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
<button class="notify-btn" onclick="notifyRelease('{{ release.version }}', this)" title="Wyślij powiadomienia o tej wersji">
🔔 Powiadom
</button>
{% endif %}
</div>
</div>
<div class="release-changes">
{% if release.new %}
<div class="change-category new">
<h4>Nowości</h4>
<ul class="change-list">
{% for item in release.new %}
<li{% if item.startswith('') %} class="release-highlight"{% endif %}>{{ render_item(item, release) }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if release.improve %}
<div class="change-category improve">
<h4>Ulepszenia</h4>
<ul class="change-list">
{% for item in release.improve %}
<li{% if item.startswith('') %} class="release-highlight"{% endif %}>{{ render_item(item, release) }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if release.fix %}
<div class="change-category fix">
<h4>Naprawione</h4>
<ul class="change-list">
{% for item in release.fix %}
<li>{{ item|safe }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if release.security %}
<div class="change-category security">
<h4>Bezpieczeństwo</h4>
<ul class="change-list">
{% for item in release.security %}
<li>{{ item|safe }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if release.beta %}
<div class="change-category beta">
<h4>W fazie testów (BETA)</h4>
<ul class="change-list">
{% for item in release.beta %}
<li>{{ item|safe }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Confirm Modal -->
<div class="confirm-modal-overlay" id="confirmModal">
<div class="confirm-modal">
<div class="confirm-modal-icon">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path 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"/>
</svg>
</div>
<h3 class="confirm-modal-title" id="confirmTitle">Wyślij powiadomienia</h3>
<p class="confirm-modal-message" id="confirmMessage">Czy na pewno chcesz wysłać powiadomienia?</p>
<p class="confirm-modal-info" id="confirmInfo"></p>
<div class="confirm-modal-actions">
<button type="button" class="btn btn-outline" onclick="closeConfirmModal()">Anuluj</button>
<button type="button" class="btn btn-primary" id="confirmButton">Wyślij</button>
</div>
</div>
</div>
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1100; display: flex; flex-direction: column; gap: 10px;"></div>
<style>
.confirm-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 2000;
justify-content: center;
align-items: center;
animation: fadeIn 0.2s ease;
}
.confirm-modal-overlay.active {
display: flex;
}
.confirm-modal {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
max-width: 420px;
width: 90%;
text-align: center;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
animation: slideUp 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.confirm-modal-icon {
width: 56px;
height: 56px;
border-radius: 50%;
background: #dbeafe;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto var(--spacing-md);
}
.confirm-modal-icon svg {
width: 28px;
height: 28px;
color: #2563eb;
}
.confirm-modal-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.confirm-modal-message {
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
}
.confirm-modal-info {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-lg);
}
.confirm-modal-actions {
display: flex;
gap: var(--spacing-md);
justify-content: center;
}
.toast { padding: 12px 20px; border-radius: var(--radius); background: var(--surface); border-left: 4px solid var(--primary); box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 10px; animation: toastIn 0.3s ease; }
.toast.success { border-left-color: var(--success); }
.toast.error { border-left-color: var(--error); }
@keyframes toastIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
</style>
{% endblock %}
{% block extra_js %}
// Toast function
function showToast(message, type = 'info', duration = 4000) {
const container = document.getElementById('toastContainer');
const icons = { success: '✓', error: '✕', warning: '⚠', info: '' };
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `<span style="font-size:1.2em">${icons[type]||''}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration);
}
// Confirm modal
let confirmCallback = null;
function showConfirmModal(title, message, info, onConfirm) {
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
document.getElementById('confirmInfo').textContent = info || '';
document.getElementById('confirmModal').classList.add('active');
confirmCallback = onConfirm;
}
function closeConfirmModal() {
document.getElementById('confirmModal').classList.remove('active');
confirmCallback = null;
}
document.getElementById('confirmButton').addEventListener('click', function() {
if (confirmCallback) {
confirmCallback();
}
closeConfirmModal();
});
document.getElementById('confirmModal').addEventListener('click', function(e) {
if (e.target === this) {
closeConfirmModal();
}
});
{% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
function notifyRelease(version, btn) {
showConfirmModal(
'Wyślij powiadomienia',
`Czy na pewno chcesz wysłać powiadomienia o wersji ${version}?`,
'Powiadomienie pojawi się przy ikonce dzwoneczka u wszystkich użytkowników.',
function() {
btn.disabled = true;
btn.innerHTML = '⏳ Wysyłanie...';
// Get first 3 highlights from the release card
const card = btn.closest('.release-card');
const highlights = [];
const items = card.querySelectorAll('.change-category.new .change-list li');
items.forEach((item, i) => {
if (i < 3) highlights.push(item.textContent.trim());
});
fetch('/admin/notify-release', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({
version: version,
highlights: highlights
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
btn.innerHTML = '✅ Wysłano';
btn.classList.add('sent');
showToast(data.message, 'success');
} else {
btn.innerHTML = '❌ Błąd';
btn.disabled = false;
showToast('Błąd: ' + data.error, 'error');
}
})
.catch(error => {
btn.innerHTML = '❌ Błąd';
btn.disabled = false;
showToast('Błąd połączenia', 'error');
});
}
);
}
{% endif %}
{% endblock %}