Quelltext des aktuellen Standes
<?php
// Standardwerte setzen
$topic = $topic ?? null;
$topic_ancestors = $topic_ancestors ?? [];
$versions = $versions ?? [];
$can_restore = $can_restore ?? false;
$this->layout('layouts/main', [
'page_title' => 'Versionen: ' . $page['title'],
'breadcrumbs' => [],
'full_width' => true
]);
?>
<div class="versions-fullwidth">
<div class="content-header">
<div>
<h1><span aria-hidden="true">📅</span> <?= $this->te('Versionshistorie') ?></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item"><a href="/topics"><?= $this->te('Themengebiete') ?></a></li>
<?php foreach ($topic_ancestors as $ancestor): ?>
<li class="breadcrumb-item"><a href="/topics/<?= $ancestor['id'] ?>"><?= $this->e($ancestor['name']) ?></a></li>
<?php endforeach; ?>
<?php if ($topic): ?>
<li class="breadcrumb-item"><a href="/topics/<?= $topic['id'] ?>"><?= $this->e($topic['name']) ?></a></li>
<?php endif; ?>
<li class="breadcrumb-item"><a href="/pages/<?= $page['id'] ?>"><?= $this->e($page['title']) ?></a></li>
<li class="breadcrumb-item active"><?= $this->te('Versionen') ?></li>
</ol>
</div>
<div class="content-header-actions">
<a href="/pages/<?= $page['id'] ?>" class="btn btn-default btn-sm"><?= $this->te('← Zurück zur Seite') ?></a>
</div>
</div>
<div class="versions-body">
<div class="versions-layout">
<!-- Versionsliste -->
<main class="versions-main">
<div class="versions-list">
<?php foreach ($versions as $index => $version): ?>
<?php $isCurrent = $version['id'] == $page['current_version_id']; ?>
<article class="version-card <?= $isCurrent ? 'current' : '' ?>">
<div class="version-header">
<div class="version-info">
<span class="version-number">Version <?= $version['version_number'] ?></span>
<?php if ($isCurrent): ?>
<span class="status-pill status-pill--success"><?= $this->te('Aktuell') ?></span>
<?php else: ?>
<span class="status-pill status-pill--neutral"><?= $this->te('Historisch') ?></span>
<?php endif; ?>
</div>
<div class="version-meta">
<span class="meta-date">
<span aria-hidden="true">📅</span> <?= date('d.m.Y H:i', strtotime($version['created_at'])) ?>
</span>
<span class="meta-author">
<span aria-hidden="true">👤</span> <?= $this->e($version['created_by_name'] ?? 'Unbekannt') ?>
</span>
</div>
</div>
<?php if (!empty($version['change_reason'])): ?>
<div class="version-reason">
<strong><?= $this->te('Änderung:') ?></strong> <?= $this->e($version['change_reason']) ?>
</div>
<?php endif; ?>
<div class="version-actions">
<a href="/pages/<?= $page['id'] ?>/versions/<?= $version['id'] ?>" class="btn btn-default btn-sm"
aria-label="Version <?= $version['version_number'] ?> anzeigen">
<span aria-hidden="true">👁</span> <?= $this->te('Anzeigen') ?>
</a>
<?php if (!$isCurrent && $can_restore): ?>
<form action="/pages/<?= $page['id'] ?>/versions/<?= $version['id'] ?>/restore" method="POST"
class="inline-form js-restore-form" data-version="<?= $version['version_number'] ?>">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<button type="submit" class="btn btn-warning btn-sm"
aria-label="Version <?= $version['version_number'] ?> wiederherstellen">
<span aria-hidden="true">↻</span> <?= $this->te('Wiederherstellen') ?>
</button>
</form>
<?php endif; ?>
<?php if ($index > 0): ?>
<a href="/pages/<?= $page['id'] ?>/versions/compare?v1=<?= $version['id'] ?>&v2=<?= $versions[$index - 1]['id'] ?>"
class="btn btn-default btn-sm"
aria-label="Version <?= $version['version_number'] ?> mit Version <?= $versions[$index - 1]['version_number'] ?> vergleichen">
<span aria-hidden="true">↔</span> <?= $this->te('Vergleichen') ?>
</a>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
<?php if (empty($versions)): ?>
<div class="empty-state">
<span class="empty-icon" aria-hidden="true">📅</span>
<h3><?= $this->te('Keine Versionen') ?></h3>
<p><?= $this->te('Für diese Seite wurden noch keine Versionen gespeichert.') ?></p>
</div>
<?php endif; ?>
</div>
</main>
<!-- Sidebar -->
<aside class="versions-sidebar">
<!-- Statistik -->
<div class="sidebar-card">
<h3><span aria-hidden="true">📊</span> <?= $this->te('Übersicht') ?></h3>
<dl class="stats-list">
<dt><?= $this->te('Versionen') ?></dt>
<dd><?= count($versions) ?></dd>
<dt><?= $this->te('Aktuelle Version') ?></dt>
<dd>v<?= $page['version_number'] ?? 1 ?></dd>
<dt><?= $this->te('Erstellt') ?></dt>
<dd><?= date('d.m.Y', strtotime($page['created_at'])) ?></dd>
</dl>
</div>
<!-- Vergleichs-Tool -->
<?php if (count($versions) >= 2): ?>
<div class="sidebar-card">
<h3><span aria-hidden="true">↔</span> <?= $this->te('Versionen vergleichen') ?></h3>
<form action="/pages/<?= $page['id'] ?>/versions/compare" method="GET" class="compare-form" id="compareForm" novalidate>
<div class="form-group">
<label for="v1"><?= $this->te('Aeltere Version:') ?></label>
<select name="v1" id="v1" class="form-control">
<?php foreach ($versions as $v): ?>
<option value="<?= $v['id'] ?>">
v<?= $v['version_number'] ?> (<?= date('d.m.Y', strtotime($v['created_at'])) ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="v2"><?= $this->te('Neuere Version:') ?></label>
<select name="v2" id="v2" class="form-control">
<?php foreach ($versions as $v): ?>
<option value="<?= $v['id'] ?>" <?= $v['id'] == ($page['current_version_id'] ?? 0) ? 'selected' : '' ?>>
v<?= $v['version_number'] ?> (<?= date('d.m.Y', strtotime($v['created_at'])) ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<p class="compare-error" id="compareError" role="alert" hidden></p>
<button type="submit" class="btn btn-primary btn-block">
<?= $this->te('Vergleichen') ?>
</button>
</form>
</div>
<?php endif; ?>
<!-- Zurück zur Seite -->
<div class="sidebar-card">
<h3><span aria-hidden="true">📄</span> <?= $this->te('Seite') ?></h3>
<a href="/pages/<?= $page['id'] ?>" class="page-link">
<span class="page-icon" aria-hidden="true">📄</span>
<span class="page-title"><?= $this->e($page['title']) ?></span>
</a>
</div>
</aside>
</div>
</div>
</div>
<!-- Bestaetigung fuer die Wiederherstellung -->
<div class="modal-overlay" id="restoreModal" style="display:none;" role="dialog" aria-modal="true" aria-labelledby="restoreModalTitle">
<div class="modal-card">
<div class="modal-header">
<h2 id="restoreModalTitle"><?= $this->te('Version wiederherstellen') ?></h2>
<button type="button" class="modal-close" id="restoreModalClose" aria-label="<?= $this->te('Schließen') ?>">×</button>
</div>
<div class="modal-body">
<p id="restoreModalText"><?= $this->te('Möchten Sie diese Version wiederherstellen?') ?></p>
<p><?= $this->t('Beim Wiederherstellen wird eine <strong>neue Version</strong> mit diesem Inhalt angelegt. Alle bisherigen Versionen bleiben erhalten.') ?></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" id="restoreModalCancel"><?= $this->te('Abbrechen') ?></button>
<button type="button" class="btn btn-primary" id="restoreModalConfirm"><?= $this->te('Wiederherstellen') ?></button>
</div>
</div>
</div>
<style>
.versions-fullwidth {
}
/* Body */
.versions-body {
padding: 2rem;
background: var(--slate-50);
min-height: calc(100vh - 300px);
}
.versions-layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
/* Main */
.versions-main {
min-width: 0;
}
.versions-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
/* Version Card */
.version-card {
background: var(--color-bg);
border-radius: 0.75rem;
padding: 1.25rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
border-left: 4px solid var(--gray-200);
transition: all 0.2s;
}
.version-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.version-card.current {
border-left-color: var(--green-500);
background: linear-gradient(135deg, var(--green-50) 0%, var(--green-100) 100%);
}
.version-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.version-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.version-number {
font-size: 1.1rem;
font-weight: 600;
color: var(--gray-900);
}
.version-meta {
display: flex;
gap: 1rem;
font-size: 0.85rem;
color: var(--gray-500);
}
.version-reason {
padding: 0.75rem;
background: var(--gray-50);
border-radius: 0.5rem;
font-size: 0.9rem;
color: var(--gray-700);
margin-bottom: 0.75rem;
}
.version-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.inline-form {
display: inline;
}
/* Sidebar */
.versions-sidebar {
display: flex;
flex-direction: column;
gap: 1rem;
}
.sidebar-card {
background: var(--color-bg);
border-radius: 0.75rem;
padding: 1.25rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.sidebar-card h3 {
margin: 0 0 1rem 0;
font-size: 0.9rem;
font-weight: 600;
color: var(--gray-700);
}
/* Stats List */
.stats-list {
margin: 0;
}
.stats-list dt {
font-size: 0.75rem;
color: var(--gray-500);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-top: 0.75rem;
}
.stats-list dt:first-child {
margin-top: 0;
}
.stats-list dd {
margin: 0.25rem 0 0 0;
font-size: 1.1rem;
font-weight: 600;
color: var(--gray-900);
}
.compare-form label {
display: block;
font-size: 0.85rem;
color: var(--gray-700);
margin-bottom: 0.375rem;
}
/* Hinweis bei ungueltiger Versionsauswahl */
.compare-error {
margin: 0 0 0.75rem 0;
font-size: 0.85rem;
color: var(--red-600);
}
.compare-error[hidden] {
display: none;
}
/* Page Link */
.page-link {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: var(--gray-50);
border-radius: 0.5rem;
text-decoration: none;
transition: all 0.15s;
}
.page-link:hover {
background: var(--blue-50);
text-decoration: none;
}
.page-icon {
font-size: 1.5rem;
}
.page-title {
font-weight: 500;
color: var(--gray-900);
}
.empty-state h3 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
color: var(--gray-700);
}
/* Responsive */
@media (max-width: 1024px) {
.versions-layout {
grid-template-columns: 1fr;
}
.versions-sidebar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
}
@media (max-width: 768px) {
.version-header {
flex-direction: column;
}
.version-meta {
flex-direction: column;
gap: 0.25rem;
}
.versions-sidebar {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.version-card {
padding: 1rem;
}
.version-number {
font-size: 1rem;
}
.version-actions .btn-sm {
font-size: 0.75rem;
padding: 0.3rem 0.5rem;
}
.sidebar-card {
padding: 1rem;
}
}
</style>
<script>
(function () {
/* Vergleichs-Formular: zwei verschiedene Versionen erzwingen und
die Auswahl vor dem Absenden auf das Paar aelter -> neuer normalisieren.
Beide Selects listen die Versionen in derselben Reihenfolge (neueste zuerst),
daher gilt: groesserer Options-Index = aeltere Version. */
var compareForm = document.getElementById('compareForm');
if (compareForm) {
var v1 = document.getElementById('v1');
var v2 = document.getElementById('v2');
var compareError = document.getElementById('compareError');
var clearError = function () {
if (compareError) {
compareError.hidden = true;
compareError.textContent = '';
}
};
if (v1 && v2) {
v1.addEventListener('change', clearError);
v2.addEventListener('change', clearError);
}
compareForm.addEventListener('submit', function (event) {
if (!v1 || !v2) return;
if (v1.value === v2.value) {
event.preventDefault();
if (compareError) {
compareError.textContent = 'Bitte zwei unterschiedliche Versionen auswählen.';
compareError.hidden = false;
}
v1.focus();
return;
}
clearError();
if (v1.selectedIndex < v2.selectedIndex) {
var older = v2.value;
var newer = v1.value;
v1.value = older;
v2.value = newer;
}
});
}
/* Wiederherstellen: Bestaetigung ueber das zentrale Modal */
var restoreModal = document.getElementById('restoreModal');
if (!restoreModal) return;
var restoreText = document.getElementById('restoreModalText');
var confirmBtn = document.getElementById('restoreModalConfirm');
var cancelBtn = document.getElementById('restoreModalCancel');
var closeBtn = document.getElementById('restoreModalClose');
var pendingForm = null;
function openModal(form) {
pendingForm = form;
if (restoreText) {
var versionNumber = form.getAttribute('data-version');
restoreText.textContent = versionNumber
? 'Möchten Sie Version ' + versionNumber + ' wiederherstellen?'
: 'Möchten Sie diese Version wiederherstellen?';
}
restoreModal.style.display = 'flex';
if (confirmBtn) confirmBtn.focus();
}
function closeModal() {
restoreModal.style.display = 'none';
pendingForm = null;
}
Array.prototype.forEach.call(document.querySelectorAll('.js-restore-form'), function (form) {
form.addEventListener('submit', function (event) {
event.preventDefault();
openModal(form);
});
});
if (confirmBtn) {
confirmBtn.addEventListener('click', function () {
if (!pendingForm) return;
var form = pendingForm;
closeModal();
form.submit();
});
}
if (cancelBtn) cancelBtn.addEventListener('click', closeModal);
if (closeBtn) closeBtn.addEventListener('click', closeModal);
restoreModal.addEventListener('click', function (event) {
if (event.target === restoreModal) closeModal();
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && restoreModal.style.display === 'flex') closeModal();
});
})();
</script>