Files
urlshort-qr-gen/templates/stats/show.php
T
florian.wilde fabaef1f2b feat(ui): Serverseitig gerenderte Oberflaeche ohne Build-Schritt
Eigenes CSS statt Framework, Vanilla-JavaScript, heller und dunkler
Modus. Ohne JavaScript bleibt alles bedienbar; das Liniendiagramm
faellt auf eine selbst gezeichnete Balkendarstellung zurueck, wenn das
CDN nicht erreichbar ist.

Chart.js ist auf eine Version festgelegt und per Subresource Integrity
an seinen Hash gebunden.

Die Domain-Seite enthaelt eine Schritt-fuer-Schritt-Anleitung fuer DNS,
Hosting-Panel und Zertifikat samt Fehlersuche.
2026-08-03 18:11:40 +02:00

159 lines
5.8 KiB
PHP
Raw 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.
<?php
/**
* @var array<string,mixed> $link
* @var string $shortUrl
* @var int $days
* @var int $totalVisits
* @var list<array{date:string,clicks:int}> $series
* @var list<array{label:string,clicks:int}> $referrers
* @var list<array{label:string,clicks:int}> $devices
* @var list<array{label:string,clicks:int}> $browsers
* @var list<array{label:string,clicks:int}> $platforms
* @var list<array<string,mixed>> $recent
*/
$id = (int) $link['id'];
$rangeLabels = [7 => '7 Tage', 30 => '30 Tage', 90 => '90 Tage', 365 => '1 Jahr'];
/** Balkenliste rendern. */
$bars = static function (array $rows) use ($e): string {
if ($rows === []) {
return '<p class="muted">Noch keine Daten.</p>';
}
$max = max(array_map(static fn (array $row): int => $row['clicks'], $rows)) ?: 1;
$html = '<ul class="bars">';
foreach ($rows as $row) {
$html .= sprintf(
'<li class="bars__row"><span class="bars__label" title="%1$s">%1$s</span>'
. '<span class="bars__track"><span class="bars__fill" style="width:%2$.1f%%"></span></span>'
. '<span class="bars__value">%3$d</span></li>',
$e($row['label']),
$row['clicks'] / $max * 100,
$row['clicks'],
);
}
return $html . '</ul>';
};
?>
<div class="page-head">
<div>
<h1>Statistik</h1>
<p class="muted">
<button type="button" class="linkcopy mono" data-copy="<?= $e($shortUrl) ?>"
title="In die Zwischenablage kopieren"><?= $e($shortUrl) ?></button>
→
<a href="<?= $e($link['target_url']) ?>" target="_blank" rel="noopener noreferrer nofollow"
class="small"><?= $e(mb_strimwidth((string) $link['target_url'], 0, 70, '…')) ?></a>
</p>
</div>
<div class="page-head__actions">
<a class="btn" href="<?= $e($url('/admin/links/' . $id . '/qr')) ?>">QR-Code</a>
<a class="btn btn--ghost" href="<?= $e($url('/admin/links/' . $id . '/edit')) ?>">Bearbeiten</a>
<a class="btn btn--ghost" href="<?= $e($url('/admin/links')) ?>">Zur Liste</a>
</div>
</div>
<div class="stats-grid">
<div class="stat">
<span class="stat__label">Klicks gesamt</span>
<span class="stat__value"><?= number_format((int) $link['click_count'], 0, ',', '.') ?></span>
</div>
<div class="stat">
<span class="stat__label">Im Zeitraum</span>
<span class="stat__value"><?= number_format(array_sum(array_column($series, 'clicks')), 0, ',', '.') ?></span>
</div>
<div class="stat">
<span class="stat__label">Erstellt</span>
<span class="stat__value stat__value--sm">
<?= $e(date('d.m.Y', strtotime((string) $link['created_at'] . ' UTC') ?: time())) ?>
</span>
</div>
<div class="stat">
<span class="stat__label">Letzter Klick</span>
<span class="stat__value stat__value--sm">
<?= $link['last_clicked_at'] !== null
? $e(date('d.m.Y H:i', strtotime((string) $link['last_clicked_at'] . ' UTC') ?: time()))
: '–' ?>
</span>
</div>
</div>
<section class="card">
<div class="card__head-row">
<h2>Klicks im Zeitverlauf</h2>
<div class="segmented" role="group" aria-label="Zeitraum">
<?php foreach ($rangeLabels as $value => $label): ?>
<a href="<?= $e($url('/admin/links/' . $id . '/stats?days=' . $value)) ?>"
class="<?= $days === $value ? 'is-active' : '' ?>"><?= $e($label) ?></a>
<?php endforeach; ?>
</div>
</div>
<div class="chart" data-chart="line" data-series="<?= $e(json_encode($series, JSON_UNESCAPED_SLASHES)) ?>">
<canvas height="110" aria-label="Liniendiagramm der Klicks pro Tag"></canvas>
<noscript><p class="muted">Für das Diagramm wird JavaScript benötigt.</p></noscript>
</div>
</section>
<div class="grid grid--2">
<section class="card">
<h2>Top-Referrer</h2>
<?= $bars($referrers) ?>
</section>
<section class="card">
<h2>Gerätetyp</h2>
<?= $bars($devices) ?>
</section>
<section class="card">
<h2>Browser</h2>
<?= $bars($browsers) ?>
</section>
<section class="card">
<h2>Plattform</h2>
<?= $bars($platforms) ?>
</section>
</div>
<section class="card">
<h2>Letzte Aufrufe</h2>
<?php if ($recent === []): ?>
<p class="muted">Noch keine Aufrufe erfasst.</p>
<?php else: ?>
<div class="table-wrap">
<table class="table table--compact">
<thead>
<tr>
<th scope="col">Zeitpunkt</th>
<th scope="col">Referrer</th>
<th scope="col">Gerät</th>
<th scope="col">Browser</th>
<th scope="col">Plattform</th>
</tr>
</thead>
<tbody>
<?php foreach ($recent as $visit): ?>
<tr>
<td class="nowrap"><?= $e(date('d.m.Y H:i:s', strtotime((string) $visit['visited_at'] . ' UTC') ?: time())) ?></td>
<td class="muted"><?= $e($visit['referrer_host'] ?? 'direkt') ?></td>
<td><?= $e(App\Support\UserAgentParser::deviceLabel((string) $visit['device_type'])) ?></td>
<td><?= $e($visit['browser']) ?></td>
<td><?= $e($visit['platform']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<p class="muted small">
Es werden weder IP-Adressen noch personenbezogene Merkmale gespeichert – nur Zeitpunkt, Referrer
und die aus dem User-Agent abgeleiteten Kategorien.
</p>
</section>