b9bcbf9198
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
407 lines
21 KiB
PHP
407 lines
21 KiB
PHP
<?php
|
|
/**
|
|
* Foto-Backend für die Schulbuch-Foto-Slots
|
|
* ------------------------------------------
|
|
* URL: /foto-upload (offen, unverlinkt — für Thomas' Foto-Nachschub)
|
|
*
|
|
* Liest alle buchBild()-Slots direkt aus den Kapitel-Dateien (pages/buch-*.php),
|
|
* zeigt pro Slot die Foto-Beschreibung und ein Upload-Formular (mehrere Dateien
|
|
* + Notiz). Uploads landen UNVERARBEITET in uploads/buch-fotos/<sim>/ — dieser
|
|
* Ordner ist per .htaccess gesperrt (Originale enthalten EXIF/GPS!).
|
|
* Atlas holt sie vom Server, strippt EXIF, skaliert und baut sie in die Slots ein.
|
|
*/
|
|
require_once __DIR__ . '/../php/config/app.php';
|
|
$bp = BASE_PATH;
|
|
|
|
/* ---------- Zugang: eingeloggte:r Admin ODER PIN (Ausnahme, vorläufig) ---------- */
|
|
require_once __DIR__ . '/../php/lib/Session.php';
|
|
Session::start();
|
|
$__isAdmin = !empty($_SESSION['admin_id']);
|
|
|
|
/* ---------- PIN-Schranke (im Browser gespeichert, 1 Jahr) ---------- */
|
|
$PIN = '0815';
|
|
$pinFehler = false;
|
|
if (isset($_POST['pin'])) {
|
|
if ($_POST['pin'] === $PIN) {
|
|
setcookie('ggs_foto_pin', $PIN, time() + 60 * 60 * 24 * 365, '/');
|
|
header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
|
|
exit;
|
|
}
|
|
$pinFehler = true;
|
|
}
|
|
if (!$__isAdmin && ($_COOKIE['ggs_foto_pin'] ?? '') !== $PIN) {
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>📷 Foto-Backend · PIN</title>
|
|
<style>
|
|
body { background: #edeae0; margin: 0; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: #2f3e46;
|
|
min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
.box { background: #fdfbf4; border-radius: 14px; box-shadow: 0 4px 18px rgba(47,62,70,.15); padding: 30px 34px; text-align: center; max-width: 320px; }
|
|
.box h1 { font-size: 20px; margin: 0 0 6px; }
|
|
.box p { color: #6b6659; font-size: 14px; margin: 0 0 16px; }
|
|
input[type=password] { width: 130px; text-align: center; font-size: 26px; letter-spacing: 8px; border: 1.5px solid #c9c2ae;
|
|
border-radius: 10px; padding: 10px 6px; background: #fff; }
|
|
button { display: block; margin: 14px auto 0; background: #4f7f5e; color: #fff; border: 0; border-radius: 10px;
|
|
padding: 11px 30px; font-size: 15px; font-weight: 800; cursor: pointer; }
|
|
.err { color: #a33227; font-weight: 700; font-size: 14px; margin-top: 10px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<form class="box" method="post">
|
|
<h1>📷 Foto-Backend</h1>
|
|
<p>Bitte PIN eingeben — dein Browser merkt sie sich.</p>
|
|
<input type="password" name="pin" inputmode="numeric" pattern="[0-9]*" maxlength="8" autofocus autocomplete="current-password">
|
|
<button type="submit">Öffnen</button>
|
|
<?php if ($pinFehler): ?><div class="err">PIN stimmt nicht.</div><?php endif; ?>
|
|
</form>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
exit;
|
|
}
|
|
|
|
$UPLOAD_BASE = __DIR__ . '/../uploads/buch-fotos';
|
|
$MAX_BYTES = 40 * 1024 * 1024; // 40 MB je Datei
|
|
$MAX_FILES = 12; // je Absenden
|
|
$ALLOWED_EXT = ['jpg','jpeg','png','heic','heif','webp','tif','tiff'];
|
|
|
|
/* ---------- Bild-Auslieferung (nur mit PIN; der Ordner selbst bleibt gesperrt) ---------- */
|
|
if (isset($_GET['img']) || isset($_GET['thumb'])) {
|
|
$req = $_GET['img'] ?? $_GET['thumb'];
|
|
$sim = preg_replace('/[^a-z0-9-]/', '', dirname($req));
|
|
$file = basename($req);
|
|
$pfad = "{$UPLOAD_BASE}/{$sim}/{$file}";
|
|
if ($sim === '' || $file === '' || strpos($file, '..') !== false || !is_file($pfad)) {
|
|
http_response_code(404); exit('nicht gefunden');
|
|
}
|
|
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
|
|
$mime = ['jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp',
|
|
'heic' => 'image/heic', 'heif' => 'image/heif', 'tif' => 'image/tiff', 'tiff' => 'image/tiff'][$ext] ?? 'application/octet-stream';
|
|
if (isset($_GET['thumb'])) {
|
|
$tdir = "{$UPLOAD_BASE}/.thumbs";
|
|
if (!is_dir($tdir)) mkdir($tdir, 0775, true);
|
|
$tpfad = "{$tdir}/{$sim}__{$file}.jpg";
|
|
if (!is_file($tpfad) || filemtime($tpfad) < filemtime($pfad)) {
|
|
$src = @imagecreatefromstring((string)@file_get_contents($pfad));
|
|
if ($src) {
|
|
// iPhone/Kamera-Fotos: EXIF-Orientierung berücksichtigen
|
|
if (in_array($ext, ['jpg', 'jpeg'], true) && function_exists('exif_read_data')) {
|
|
$exif = @exif_read_data($pfad);
|
|
$o = (int)($exif['Orientation'] ?? 1);
|
|
if ($o === 3) $src = imagerotate($src, 180, 0);
|
|
elseif ($o === 6) $src = imagerotate($src, -90, 0);
|
|
elseif ($o === 8) $src = imagerotate($src, 90, 0);
|
|
}
|
|
$w = imagesx($src); $h = imagesy($src);
|
|
$f = min(1, 340 / max($w, $h));
|
|
$tw = max(1, (int)round($w * $f)); $th = max(1, (int)round($h * $f));
|
|
$dst = imagecreatetruecolor($tw, $th);
|
|
imagecopyresampled($dst, $src, 0, 0, 0, 0, $tw, $th, $w, $h);
|
|
imagejpeg($dst, $tpfad, 82);
|
|
imagedestroy($src); imagedestroy($dst);
|
|
}
|
|
}
|
|
if (is_file($tpfad)) { $pfad = $tpfad; $mime = 'image/jpeg'; }
|
|
// sonst (z. B. HEIC ohne GD-Support): Original ausliefern — Safari/iPad zeigt es nativ
|
|
}
|
|
header('Content-Type: ' . $mime);
|
|
header('Content-Length: ' . (string)filesize($pfad));
|
|
header('Cache-Control: private, max-age=86400');
|
|
readfile($pfad);
|
|
exit;
|
|
}
|
|
|
|
/* ---------- Slots aus den Kapiteln parsen ---------- */
|
|
$ORDER = ['tourismustal','tourismusregion','kofferdetektiv','busfahrt','logistik',
|
|
'weltkueche','farmer','klima','fluss','energiemanager','heli',
|
|
'sonnensystem','eu-werkstatt'];
|
|
$KAPITEL = [];
|
|
foreach ($ORDER as $id) {
|
|
$file = __DIR__ . "/buch-{$id}.php";
|
|
if (!is_file($file)) continue;
|
|
$src = file_get_contents($file);
|
|
$titel = preg_match('/<h1>(.*?)<\/h1>/s', $src, $m) ? trim(strip_tags($m[1])) : $id;
|
|
preg_match_all("/buchBild\(\s*'((?:[^'\\\\]|\\\\.)*)'\s*,\s*'((?:[^'\\\\]|\\\\.)*)'/s", $src, $mm, PREG_SET_ORDER);
|
|
$slots = [];
|
|
foreach ($mm as $b) {
|
|
$slug = stripcslashes($b[1]);
|
|
$vorschlag = stripcslashes($b[2]);
|
|
// Leicht- und Normal-Version rufen denselben Slot auf → längste Beschreibung gewinnt
|
|
if (!isset($slots[$slug]) || mb_strlen($vorschlag) > mb_strlen($slots[$slug])) {
|
|
$slots[$slug] = $vorschlag;
|
|
}
|
|
}
|
|
if ($slots) $KAPITEL[$id] = ['titel' => $titel, 'slots' => $slots];
|
|
}
|
|
|
|
/* ---------- Helfer ---------- */
|
|
function slotBildPfad(string $sim, string $slug): ?string {
|
|
foreach (['jpg','jpeg','webp','png'] as $ext) {
|
|
$rel = "assets/img/buch/{$sim}/{$slug}.{$ext}";
|
|
if (is_file(__DIR__ . '/../' . $rel)) return $rel;
|
|
}
|
|
return null;
|
|
}
|
|
function pendingUploads(string $base, string $sim, string $slug): array {
|
|
$dir = "{$base}/{$sim}";
|
|
if (!is_dir($dir)) return [];
|
|
$out = [];
|
|
foreach (glob($dir . '/' . $slug . '__*') as $f) {
|
|
if (substr($f, -9) === '.note.txt') continue;
|
|
$out[] = ['name' => basename($f), 'kb' => (int)round(filesize($f) / 1024)];
|
|
}
|
|
return $out;
|
|
}
|
|
function pendingNotes(string $base, string $sim, string $slug): array {
|
|
$dir = "{$base}/{$sim}";
|
|
if (!is_dir($dir)) return [];
|
|
$out = [];
|
|
foreach (glob($dir . '/' . $slug . '__*.note.txt') as $f) {
|
|
$t = trim((string)file_get_contents($f));
|
|
if ($t !== '') $out[] = $t;
|
|
}
|
|
return $out;
|
|
}
|
|
function safeName(string $s): string {
|
|
$s = preg_replace('/[^A-Za-z0-9._-]+/', '-', $s);
|
|
return trim(mb_substr($s, 0, 80), '-.') ?: 'foto';
|
|
}
|
|
|
|
/* ---------- Löschen eines wartenden Uploads (PRG) ---------- */
|
|
$fehler = [];
|
|
$geloescht = 0;
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['aktion'] ?? '') === 'loeschen') {
|
|
$sim = preg_replace('/[^a-z0-9-]/', '', $_POST['sim'] ?? '');
|
|
$file = basename($_POST['file'] ?? '');
|
|
$pfad = "{$UPLOAD_BASE}/{$sim}/{$file}";
|
|
// nur echte Upload-Dateien (Muster <slug>__…), keine Notizen, kein Pfad-Ausbruch
|
|
if ($sim !== '' && $file !== '' && strpos($file, '..') === false
|
|
&& substr($file, -9) !== '.note.txt' && is_file($pfad)) {
|
|
@unlink($pfad);
|
|
// zugehörigen Thumbnail-Cache mitlöschen
|
|
@unlink("{$UPLOAD_BASE}/.thumbs/{$sim}__{$file}.jpg");
|
|
$geloescht = 1;
|
|
} else {
|
|
$fehler[] = 'Foto zum Löschen nicht gefunden.';
|
|
}
|
|
if ($geloescht) {
|
|
$anker = $sim . '-' . preg_replace('/__.*/', '', $file);
|
|
header("Location: ?del=1&sim=" . urlencode($sim) . "#{$anker}");
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/* ---------- Upload verarbeiten (PRG-Muster) ---------- */
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['aktion'] ?? '') !== 'loeschen') {
|
|
$sim = $_POST['sim'] ?? '';
|
|
$slug = preg_replace('/[^a-z0-9-]/', '', $_POST['slug'] ?? '');
|
|
$note = trim($_POST['note'] ?? '');
|
|
|
|
$gueltig = ($sim === 'sonstiges' && $slug !== '')
|
|
|| (isset($KAPITEL[$sim]) && isset($KAPITEL[$sim]['slots'][$slug]));
|
|
if (!$gueltig) {
|
|
$fehler[] = 'Unbekannter Foto-Slot.';
|
|
} else {
|
|
$dir = "{$UPLOAD_BASE}/{$sim}";
|
|
if (!is_dir($dir)) mkdir($dir, 0775, true);
|
|
// Ordner absichern: Originale (mit GPS!) dürfen nie öffentlich abrufbar sein
|
|
if (!is_file($UPLOAD_BASE . '/.htaccess')) {
|
|
file_put_contents($UPLOAD_BASE . '/.htaccess', "Require all denied\n");
|
|
}
|
|
$ts = date('Ymd-His');
|
|
$gespeichert = 0;
|
|
if (!empty($_FILES['fotos']) && is_array($_FILES['fotos']['name'])) {
|
|
$n = min(count($_FILES['fotos']['name']), $MAX_FILES);
|
|
for ($i = 0; $i < $n; $i++) {
|
|
if ($_FILES['fotos']['error'][$i] === UPLOAD_ERR_NO_FILE) continue;
|
|
if ($_FILES['fotos']['error'][$i] !== UPLOAD_ERR_OK) { $fehler[] = 'Upload-Fehler bei Datei ' . ($i + 1) . ' (Code ' . $_FILES['fotos']['error'][$i] . ').'; continue; }
|
|
if ($_FILES['fotos']['size'][$i] > $MAX_BYTES) { $fehler[] = htmlspecialchars($_FILES['fotos']['name'][$i]) . ' ist größer als 40 MB.'; continue; }
|
|
$orig = $_FILES['fotos']['name'][$i];
|
|
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
|
|
if (!in_array($ext, $ALLOWED_EXT, true)) { $fehler[] = htmlspecialchars($orig) . ': Dateityp .' . htmlspecialchars($ext) . ' nicht erlaubt.'; continue; }
|
|
$ziel = "{$dir}/{$slug}__{$ts}__" . ($i + 1) . '__' . safeName($orig);
|
|
if (move_uploaded_file($_FILES['fotos']['tmp_name'][$i], $ziel)) $gespeichert++;
|
|
else $fehler[] = htmlspecialchars($orig) . ' konnte nicht gespeichert werden.';
|
|
}
|
|
}
|
|
if ($gespeichert === 0 && !$fehler) $fehler[] = 'Keine Datei ausgewählt.';
|
|
if ($note !== '' && ($gespeichert > 0 || $sim === 'sonstiges')) {
|
|
file_put_contents("{$dir}/{$slug}__{$ts}.note.txt", $note . "\n");
|
|
}
|
|
if ($gespeichert > 0 && !$fehler) {
|
|
header("Location: ?ok={$gespeichert}&sim=" . urlencode($sim) . "&slug=" . urlencode($slug) . "#{$sim}-{$slug}");
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
$ok = isset($_GET['ok']) ? (int)$_GET['ok'] : 0;
|
|
$okAnker = $ok ? (($_GET['sim'] ?? '') . '-' . ($_GET['slug'] ?? '')) : '';
|
|
$del = isset($_GET['del']);
|
|
|
|
/* Übersichtszahlen */
|
|
$gesamt = 0; $mitFoto = 0; $wartend = 0;
|
|
foreach ($KAPITEL as $sim => $k) {
|
|
foreach ($k['slots'] as $slug => $v) {
|
|
$gesamt++;
|
|
if (slotBildPfad($sim, $slug)) $mitFoto++;
|
|
if (pendingUploads($UPLOAD_BASE, $sim, $slug)) $wartend++;
|
|
}
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>📷 Foto-Backend · Schulbuch · GeoGraSim</title>
|
|
<style>
|
|
body { background: #edeae0; margin: 0; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: #2f3e46; }
|
|
.w { max-width: 880px; margin: 0 auto; padding: 22px 14px 90px; }
|
|
h1 { font-size: 26px; margin: 0 0 6px; }
|
|
h2 { font-size: 20px; margin: 34px 0 4px; border-bottom: 2px solid #d9d3c0; padding-bottom: 6px; }
|
|
.sub { color: #6b6659; line-height: 1.55; margin: 0 0 8px; }
|
|
.stat { display: inline-block; background: #fdfbf4; border: 1.5px solid #c9c2ae; border-radius: 999px; padding: 4px 12px; font-size: 13px; font-weight: 700; margin: 2px 6px 2px 0; }
|
|
.slot { background: #fdfbf4; border-radius: 12px; box-shadow: 0 3px 12px rgba(47,62,70,.10); padding: 16px 18px; margin: 14px 0; }
|
|
.slot.hat { border-left: 5px solid #4f7f5e; }
|
|
.slot.wartet { border-left: 5px solid #e8892b; }
|
|
.slot.offen { border-left: 5px solid #c9c2ae; }
|
|
.kopf { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
|
|
.kopf code { font-size: 12px; color: #9a958a; }
|
|
.badge { font-size: 11px; font-weight: 800; border-radius: 999px; padding: 2px 10px; }
|
|
.badge.hat { background: #dcead1; color: #2f6b46; }
|
|
.badge.wartet { background: #fbe9c8; color: #a06a12; }
|
|
.badge.offen { background: #eeeadd; color: #6b6659; }
|
|
.beschr { line-height: 1.55; font-size: 14.5px; margin: 8px 0 10px; }
|
|
.thumb { max-width: 190px; border-radius: 8px; display: block; margin: 6px 0; }
|
|
a.lightbox { cursor: zoom-in; }
|
|
.pgrid { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0; }
|
|
.pgrid .pt { position: relative; }
|
|
.pgrid .pt img { width: 110px; height: 110px; object-fit: cover; border-radius: 8px; display: block;
|
|
border: 2px solid #e8c9a0; background: #eee; }
|
|
.pgrid .pt .del { position: absolute; top: 3px; right: 3px; margin: 0; }
|
|
.pgrid .pt .del button { width: 26px; height: 26px; border-radius: 999px; border: 0; cursor: pointer;
|
|
background: rgba(163,50,39,.92); color: #fff; font-size: 15px; font-weight: 800; line-height: 1;
|
|
display: flex; align-items: center; justify-content: center; box-shadow: 0 1px 4px rgba(0,0,0,.35); }
|
|
.pgrid .pt .del button:hover { background: #a33227; }
|
|
.pnote { font-size: 13px; color: #6b6659; font-style: italic; margin: 2px 0 6px; }
|
|
#ov { position: fixed; inset: 0; background: rgba(20,26,30,.88); display: flex; align-items: center;
|
|
justify-content: center; z-index: 99; cursor: zoom-out; }
|
|
#ov[hidden] { display: none; }
|
|
#ov img { max-width: 94vw; max-height: 92vh; border-radius: 8px; box-shadow: 0 10px 40px rgba(0,0,0,.5); }
|
|
#ovx { position: fixed; top: 12px; right: 18px; color: #fff; font-size: 30px; font-weight: 800; cursor: pointer;
|
|
text-shadow: 0 1px 6px rgba(0,0,0,.7); }
|
|
form.up { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; background: #f4f1e6; border-radius: 9px; padding: 10px 12px; }
|
|
form.up input[type=file] { font-size: 13px; max-width: 100%; }
|
|
form.up input[type=text] { flex: 1 1 220px; min-width: 180px; border: 1.5px solid #c9c2ae; border-radius: 8px; padding: 8px 10px; font-size: 14px; background: #fff; }
|
|
form.up button { background: #4f7f5e; color: #fff; border: 0; border-radius: 8px; padding: 9px 18px; font-size: 14px; font-weight: 800; cursor: pointer; }
|
|
form.up button:hover { background: #3f6a4e; }
|
|
.okbox { background: #dcead1; border-left: 5px solid #4f7f5e; border-radius: 9px; padding: 11px 14px; font-weight: 700; margin: 12px 0; }
|
|
.errbox { background: #f3dcd6; border-left: 5px solid #a33227; border-radius: 9px; padding: 11px 14px; margin: 12px 0; }
|
|
.hinweis { background: #fbf6e3; border: 1.5px solid #e8c547; border-radius: 10px; padding: 10px 14px; font-size: 14px; line-height: 1.6; margin: 12px 0 4px; }
|
|
a { color: #4f7f5e; }
|
|
.toc { font-size: 13.5px; line-height: 2; background: #fdfbf4; border-radius: 12px; padding: 12px 16px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="w">
|
|
<h1>📷 Foto-Backend zum Schulbuch</h1>
|
|
<p class="sub">Alle Foto-Slots aus den <a href="<?= $bp ?>/buch">13 Schulbuch-Kapiteln</a> mit ihren
|
|
Foto-Wünschen. Zu jedem Slot kannst du direkt Fotos hochladen (mehrere auf einmal, auch HEIC
|
|
vom iPhone/iPad) und eine Notiz dazuschreiben — Atlas holt sie ab, entfernt die GPS-Daten,
|
|
verkleinert sie und baut sie ein.</p>
|
|
<span class="stat">📷 <?= $gesamt ?> Slots</span>
|
|
<span class="stat">✅ <?= $mitFoto ?> eingebaut</span>
|
|
<span class="stat">📥 <?= $wartend ?> mit wartenden Uploads</span>
|
|
|
|
<div class="hinweis">🔒 Hochgeladene Original-Dateien sind <b>nicht öffentlich abrufbar</b>
|
|
(Ordner gesperrt) — deine GPS-Daten bleiben privat. Veröffentlicht wird nur die bereinigte
|
|
Version im Kapitel.</div>
|
|
|
|
<?php if ($ok): ?><div class="okbox">✅ <?= $ok ?> Foto<?= $ok > 1 ? 's' : '' ?> hochgeladen — danke! Atlas kümmert sich um Zuschnitt und Einbau.</div><?php endif; ?>
|
|
<?php if ($del): ?><div class="okbox">🗑️ Foto gelöscht.</div><?php endif; ?>
|
|
<?php foreach ($fehler as $f): ?><div class="errbox">⚠️ <?= $f ?></div><?php endforeach; ?>
|
|
|
|
<div class="toc"><b>Kapitel:</b>
|
|
<?php foreach ($KAPITEL as $sim => $k): ?>
|
|
<a href="#kap-<?= $sim ?>"><?= htmlspecialchars($k['titel']) ?></a> ·
|
|
<?php endforeach; ?>
|
|
<a href="#kap-sonstiges">Sonstiges</a>
|
|
</div>
|
|
|
|
<?php foreach ($KAPITEL as $sim => $k): ?>
|
|
<h2 id="kap-<?= $sim ?>"><?= htmlspecialchars($k['titel']) ?></h2>
|
|
<?php foreach ($k['slots'] as $slug => $vorschlag):
|
|
$bild = slotBildPfad($sim, $slug);
|
|
$pend = pendingUploads($UPLOAD_BASE, $sim, $slug);
|
|
$status = $bild ? 'hat' : ($pend ? 'wartet' : 'offen');
|
|
?>
|
|
<div class="slot <?= $status ?>" id="<?= $sim ?>-<?= $slug ?>">
|
|
<div class="kopf">
|
|
<b><?= $slug ?></b>
|
|
<span class="badge <?= $status ?>"><?= $bild ? '✅ Foto eingebaut' : ($pend ? '📥 Upload wartet auf Einbau' : '📷 Foto gesucht') ?></span>
|
|
</div>
|
|
<p class="beschr"><?= htmlspecialchars($vorschlag) ?></p>
|
|
<?php if ($bild): ?><a class="lightbox" href="<?= $bp ?>/<?= $bild ?>"><img class="thumb" src="<?= $bp ?>/<?= $bild ?>" alt="" loading="lazy"></a><?php endif; ?>
|
|
<?php if ($pend): ?>
|
|
<div class="pgrid">
|
|
<?php foreach ($pend as $p): ?>
|
|
<div class="pt">
|
|
<a class="lightbox" href="?img=<?= urlencode($sim . '/' . $p['name']) ?>" title="<?= htmlspecialchars($p['name']) ?> · <?= $p['kb'] ?> KB">
|
|
<img src="?thumb=<?= urlencode($sim . '/' . $p['name']) ?>" loading="lazy" alt="">
|
|
</a>
|
|
<form method="post" class="del" onsubmit="return confirm('Dieses Foto wirklich löschen?');">
|
|
<input type="hidden" name="aktion" value="loeschen">
|
|
<input type="hidden" name="sim" value="<?= htmlspecialchars($sim) ?>">
|
|
<input type="hidden" name="file" value="<?= htmlspecialchars($p['name']) ?>">
|
|
<button type="submit" title="Foto löschen" aria-label="Foto löschen">✕</button>
|
|
</form>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php foreach (pendingNotes($UPLOAD_BASE, $sim, $slug) as $n): ?><div class="pnote">📝 <?= htmlspecialchars($n) ?></div><?php endforeach; ?>
|
|
<form class="up" method="post" enctype="multipart/form-data" action="?#<?= $sim ?>-<?= $slug ?>">
|
|
<input type="hidden" name="sim" value="<?= $sim ?>">
|
|
<input type="hidden" name="slug" value="<?= $slug ?>">
|
|
<input type="file" name="fotos[]" accept="image/*,.heic,.heif" multiple>
|
|
<input type="text" name="note" placeholder="Notiz (Ort, Jahr, was zu sehen ist … optional)" maxlength="500">
|
|
<button type="submit">⬆️ Hochladen</button>
|
|
</form>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endforeach; ?>
|
|
|
|
<h2 id="kap-sonstiges">Sonstiges / ohne Zuordnung</h2>
|
|
<div class="slot offen" id="sonstiges-foto">
|
|
<p class="beschr">Fotos, die zu keinem Slot passen, aber gut sein könnten — mit kurzer
|
|
Beschreibung, wofür du sie siehst. Atlas ordnet sie zu oder legt neue Slots an.</p>
|
|
<form class="up" method="post" enctype="multipart/form-data" action="?#sonstiges-foto">
|
|
<input type="hidden" name="sim" value="sonstiges">
|
|
<input type="hidden" name="slug" value="foto">
|
|
<input type="file" name="fotos[]" accept="image/*,.heic,.heif" multiple>
|
|
<input type="text" name="note" placeholder="Beschreibung: Was ist das, wofür könnte es passen?" maxlength="500">
|
|
<button type="submit">⬆️ Hochladen</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="ov" hidden><img id="ovimg" src="" alt=""><span id="ovx">✕</span></div>
|
|
<script>
|
|
(function () {
|
|
var ov = document.getElementById('ov'), ovimg = document.getElementById('ovimg');
|
|
function zu() { ov.hidden = true; ovimg.src = ''; document.body.style.overflow = ''; }
|
|
document.addEventListener('click', function (e) {
|
|
var a = e.target.closest('a.lightbox');
|
|
if (a) { e.preventDefault(); ovimg.src = a.href; ov.hidden = false; document.body.style.overflow = 'hidden'; return; }
|
|
if (!ov.hidden) zu();
|
|
});
|
|
document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !ov.hidden) zu(); });
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|