partials.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. <?php
  2. /**
  3. * Shared HTML fragments for the public site and the admin backoffice.
  4. * Admin pages all sit in admin/ and hardcode '../assets/...'. Public pages sit
  5. * in the document root except the gallery viewer in gallery/, so they prefix
  6. * their links with base() — '' or '../' depending on the page (see bootstrap).
  7. */
  8. declare(strict_types=1);
  9. function public_header(string $title, string $active = '', string $bodyClass = ''): void
  10. {
  11. $site = site_get();
  12. $name = $site['intro_title'] ?: config('site.name', 'Portfolio');
  13. $base = base();
  14. ?><!DOCTYPE html>
  15. <html lang="en">
  16. <head>
  17. <meta charset="utf-8">
  18. <meta name="viewport" content="width=device-width, initial-scale=1">
  19. <title><?= e($title) ?></title>
  20. <link rel="stylesheet" href="<?= e($base) ?>assets/site.css">
  21. </head>
  22. <body class="<?= e($bodyClass) ?>">
  23. <header class="nav" id="nav">
  24. <a class="nav-brand" href="<?= e($base ?: './') ?>"><?= e($name) ?></a>
  25. <nav class="nav-links">
  26. <a href="<?= e($base ?: './') ?>" class="<?= $active === 'home' ? 'active' : '' ?>">Home</a>
  27. <a href="<?= e($base) ?>showreel.php" class="<?= $active === 'showreel' ? 'active' : '' ?>">Showreel</a>
  28. <a href="<?= e($base) ?>contact.php" class="<?= $active === 'contact' ? 'active' : '' ?>">Contact</a>
  29. </nav>
  30. </header>
  31. <?php
  32. }
  33. function public_footer(): void
  34. {
  35. $base = base();
  36. ?><footer class="footer">
  37. <nav class="footer-links">
  38. <a href="<?= e($base) ?>impressum.php">Impressum</a>
  39. <a href="<?= e($base) ?>datenschutz.php">Datenschutz</a>
  40. </nav>
  41. <span>&copy; <?= date('Y') ?> <?= e(site_get()['intro_title'] ?: config('site.name', '')) ?></span>
  42. </footer>
  43. <script src="<?= e($base) ?>assets/site.js"></script>
  44. </body>
  45. </html>
  46. <?php
  47. // Last thing on every public page: nudge the gallery-archive worker along if
  48. // anything is queued. Cheap when there is nothing to do, and it flushes the
  49. // page to the visitor before doing anything slow. See app/archive.php.
  50. archive_kick();
  51. }
  52. function admin_header(string $title, string $active = ''): void
  53. {
  54. ?><!DOCTYPE html>
  55. <html lang="en">
  56. <head>
  57. <meta charset="utf-8">
  58. <meta name="viewport" content="width=device-width, initial-scale=1">
  59. <title><?= e($title) ?> · Admin</title>
  60. <link rel="stylesheet" href="../assets/site.css">
  61. </head>
  62. <body class="admin">
  63. <header class="admin-nav">
  64. <a class="nav-brand" href="index.php">Backoffice</a>
  65. <nav class="nav-links">
  66. <a href="frontpage.php" class="<?= $active === 'frontpage' ? 'active' : '' ?>">Front page</a>
  67. <a href="showreel.php" class="<?= $active === 'showreel' ? 'active' : '' ?>">Showreel</a>
  68. <a href="galleries.php" class="<?= $active === 'galleries' ? 'active' : '' ?>">Galleries</a>
  69. <a href="contact.php" class="<?= $active === 'contact' ? 'active' : '' ?>">Contact</a>
  70. <a href="settings.php" class="<?= $active === 'settings' ? 'active' : '' ?>">Settings</a>
  71. <a href="maintenance.php" class="<?= $active === 'maintenance' ? 'active' : '' ?>">Maintenance</a>
  72. <a href="../" target="_blank" rel="noopener">View site ↗</a>
  73. <a href="logout.php">Log out</a>
  74. </nav>
  75. </header>
  76. <main class="admin-main">
  77. <?php
  78. }
  79. function admin_footer(): void
  80. {
  81. ?></main>
  82. </body>
  83. </html>
  84. <?php
  85. }
  86. /**
  87. * The "resize uploads to" control, shared by the gallery create and settings
  88. * forms. $current is the gallery's stored cap in pixels, or null for Original.
  89. *
  90. * The select carries pixel values, never names, so the handler
  91. * (parse_max_resolution) never has to map a label back to a number. The custom
  92. * input is revealed by a tiny delegated script emitted once per page —
  93. * assets/admin.js is the uploader and is not loaded on galleries.php.
  94. */
  95. function resolution_field(?int $current = null): void
  96. {
  97. $isPreset = $current !== null && in_array($current, RESOLUTION_PRESETS, true);
  98. $isCustom = $current !== null && !$isPreset;
  99. ?>
  100. <label for="res">Resize uploads to <span style="text-transform:none;letter-spacing:0">(longest edge)</span></label>
  101. <div class="res-field">
  102. <select id="res" name="max_resolution">
  103. <option value="" <?= $current === null ? 'selected' : '' ?>>Original — no resize</option>
  104. <?php foreach (RESOLUTION_PRESETS as $px): ?>
  105. <option value="<?= $px ?>" <?= $current === $px ? 'selected' : '' ?>><?= e(resolution_label($px)) ?></option>
  106. <?php endforeach; ?>
  107. <option value="custom" <?= $isCustom ? 'selected' : '' ?>>Custom…</option>
  108. </select>
  109. <input type="number" name="max_resolution_custom" placeholder="e.g. 3000"
  110. min="<?= RESOLUTION_MIN ?>" max="<?= RESOLUTION_MAX ?>"
  111. value="<?= $isCustom ? (int)$current : '' ?>" <?= $isCustom ? '' : 'hidden' ?>>
  112. </div>
  113. <p class="help">
  114. Images larger than this are downscaled in the browser before uploading,
  115. which also keeps them under the server's upload limit. The re-encode
  116. drops EXIF data (camera, lens, date, location).
  117. </p>
  118. <?php
  119. static $scriptDone = false;
  120. if ($scriptDone) {
  121. return;
  122. }
  123. $scriptDone = true;
  124. ?>
  125. <script>
  126. document.addEventListener('change', function (e) {
  127. var select = e.target.closest('.res-field select');
  128. if (!select) return;
  129. var custom = select.parentNode.querySelector('input[name="max_resolution_custom"]');
  130. custom.hidden = select.value !== 'custom';
  131. if (!custom.hidden) custom.focus();
  132. });
  133. </script>
  134. <?php
  135. }
  136. /**
  137. * The "strip EXIF" checkbox, shared by the gallery create and settings forms.
  138. * $current is the gallery's stored flag.
  139. *
  140. * It is a separate control from the resolution cap rather than part of it
  141. * because the two only overlap: a capped gallery is re-encoded in the browser
  142. * and loses its metadata either way, while an "Original" gallery — the case
  143. * this exists for — keeps every byte unless this is ticked.
  144. */
  145. function strip_exif_field(bool $current = false): void
  146. {
  147. ?>
  148. <p class="help" style="margin-bottom:.4rem"><label style="display:inline;text-transform:none;letter-spacing:0">
  149. <input type="checkbox" name="strip_exif" value="1" <?= $current ? 'checked' : '' ?>>
  150. Strip EXIF metadata from uploaded images
  151. </label></p>
  152. <p class="help">
  153. Removes camera, lens, exposure, timestamp and GPS data from JPEG, PNG and
  154. WebP uploads before they are stored. The photo itself is not re-encoded.
  155. </p>
  156. <?php
  157. }
  158. /**
  159. * How this gallery treats what is uploaded to it, as a phrase for the dropzone
  160. * on the admin editor and the guest upload page ("full resolution, unmodified",
  161. * "downscaled to 2560 px on the longest edge, EXIF metadata removed", …).
  162. */
  163. function upload_treatment_text(array $gallery): string
  164. {
  165. $parts = isset($gallery['max_resolution'])
  166. ? ['downscaled to ' . (int)$gallery['max_resolution'] . ' px on the longest edge']
  167. : ['full resolution'];
  168. if (!empty($gallery['strip_exif'])) {
  169. $parts[] = 'EXIF metadata removed';
  170. } elseif (!isset($gallery['max_resolution'])) {
  171. $parts[] = 'unmodified';
  172. }
  173. return implode(', ', $parts);
  174. }
  175. /** One-shot status message helpers (flash messages via session). */
  176. function flash_set(string $msg, string $kind = 'ok'): void
  177. {
  178. session_boot();
  179. $_SESSION['flash'] = ['msg' => $msg, 'kind' => $kind];
  180. }
  181. function flash_render(): void
  182. {
  183. session_boot();
  184. if (!empty($_SESSION['flash'])) {
  185. $f = $_SESSION['flash'];
  186. unset($_SESSION['flash']);
  187. echo '<div class="flash flash-' . e($f['kind']) . '">' . e($f['msg']) . '</div>';
  188. }
  189. }