Browse Source

implementing FAQ

Medowar 1 month ago
parent
commit
643816b5cf
10 changed files with 339 additions and 4 deletions
  1. 5 0
      README.md
  2. 55 0
      admin/faq.php
  3. 10 4
      admin/index.php
  4. 99 0
      assets/css/style.css
  5. 1 0
      config.sample.php
  6. 3 0
      data/faq.json
  7. 1 0
      docs/CONFIG_REFERENCE.md
  8. 18 0
      faq.php
  9. 146 0
      includes/functions.php
  10. 1 0
      includes/header.php

+ 5 - 0
README.md

@@ -51,3 +51,8 @@ Für die Seite `Meine Bestellungen` wird eine signierte, browsergebundene Histor
 Details zur Funktion und zum Sicherheitsmodell:
 
 - `docs/ORDER_HISTORY.md`
+
+## 5) FAQ-Inhalte pflegen
+
+- FAQ-Inhalt wird aus `data/faq.json` geladen (`FAQ_FILE` in `config.php`).
+- Bearbeitung erfolgt im Admin-Bereich unter `admin/faq.php`.

+ 55 - 0
admin/faq.php

@@ -0,0 +1,55 @@
+<?php
+require_once __DIR__ . '/../config.php';
+require_once __DIR__ . '/../includes/functions.php';
+
+// Check admin login
+if (!isset($_SESSION['admin_logged_in']) || !$_SESSION['admin_logged_in']) {
+    header('Location: login.php');
+    exit;
+}
+
+$pageTitle = 'FAQ bearbeiten';
+$message = '';
+$messageType = '';
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_faq'])) {
+    $content = isset($_POST['content']) ? (string) $_POST['content'] : '';
+    saveFaqContent($content);
+    $message = 'FAQ-Inhalt wurde gespeichert.';
+    $messageType = 'success';
+}
+
+$faqContent = getFaqContent();
+
+$bodyClass = 'admin-page';
+include __DIR__ . '/../includes/header.php';
+?>
+
+<div class="admin-header">
+    <h2>FAQ bearbeiten</h2>
+    <div>
+        <a href="index.php" class="btn btn-secondary">Zurück zum Dashboard</a>
+    </div>
+</div>
+
+<?php if ($message): ?>
+    <div class="alert alert-<?php echo $messageType; ?>">
+        <?php echo htmlspecialchars($message); ?>
+    </div>
+<?php endif; ?>
+
+<div class="panel" style="padding: 2rem;">
+    <p class="mb-2">
+        Unterstützte Markdown-Syntax: <code>#</code>, <code>##</code>, <code>###</code>, <code>**fett**</code>, <code>*kursiv*</code>, Listen mit <code>-</code> oder <code>1.</code>
+    </p>
+
+    <form method="POST">
+        <div class="form-group">
+            <label for="content">FAQ-Inhalt (Markdown)</label>
+            <textarea id="content" name="content" rows="18"><?php echo htmlspecialchars($faqContent); ?></textarea>
+        </div>
+        <button type="submit" name="save_faq" class="btn">Speichern</button>
+    </form>
+</div>
+
+<?php include __DIR__ . '/../includes/footer.php'; ?>

+ 10 - 4
admin/index.php

@@ -50,12 +50,18 @@ include __DIR__ . '/../includes/header.php';
 
 <div class="admin-header">
     <h2>Admin Dashboard</h2>
-    <div>
-        <a href="products.php" class="btn">Produkte verwalten</a>
+    <div class="admin-dashboard-actions">
         <a href="reservations.php" class="btn">Reservierungen</a>
         <a href="backorders.php" class="btn">Vorbestellungen</a>
-        <a href="admins.php" class="btn">Admins verwalten</a>
-        <a href="login.php?logout=1" class="btn btn-secondary">Abmelden</a>
+        <details class="admin-actions-dropdown">
+            <summary class="btn btn-secondary">Verwaltung</summary>
+            <div class="admin-actions-menu">
+                <a href="products.php">Produkte verwalten</a>
+                <a href="faq.php">FAQ bearbeiten</a>
+                <a href="admins.php">Admins verwalten</a>
+                <a href="login.php?logout=1">Abmelden</a>
+            </div>
+        </details>
     </div>
 </div>
 

+ 99 - 0
assets/css/style.css

@@ -413,6 +413,84 @@ footer {
     margin-bottom: 2rem;
 }
 
+.admin-dashboard-actions {
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+    gap: 0.5rem;
+}
+
+.admin-actions-dropdown {
+    position: relative;
+}
+
+.admin-actions-dropdown summary {
+    list-style: none;
+}
+
+.admin-actions-dropdown summary::-webkit-details-marker {
+    display: none;
+}
+
+.admin-actions-dropdown .btn {
+    display: inline-block;
+}
+
+.admin-actions-dropdown[open] > .btn {
+    background-color: var(--brand-surface-alt);
+}
+
+.admin-actions-menu {
+    position: absolute;
+    right: 0;
+    top: calc(100% + 0.35rem);
+    min-width: 220px;
+    background: var(--brand-surface);
+    border: 1px solid var(--brand-border);
+    border-radius: 6px;
+    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.35);
+    z-index: 30;
+    padding: 0.35rem;
+}
+
+.admin-actions-menu a {
+    display: block;
+    text-decoration: none;
+    color: var(--brand-text);
+    padding: 0.55rem 0.7rem;
+    border-radius: 4px;
+}
+
+.admin-actions-menu a:hover {
+    background: var(--brand-surface-alt);
+    color: var(--brand-accent);
+}
+
+.faq-content h1,
+.faq-content h2,
+.faq-content h3 {
+    margin: 1rem 0 0.6rem;
+}
+
+.faq-content h1:first-child,
+.faq-content h2:first-child,
+.faq-content h3:first-child {
+    margin-top: 0;
+}
+
+.faq-content p + p {
+    margin-top: 0.8rem;
+}
+
+.faq-content ul,
+.faq-content ol {
+    margin: 0.7rem 0 0.9rem 1.5rem;
+}
+
+.faq-content li + li {
+    margin-top: 0.35rem;
+}
+
 .admin-stats {
     display: grid;
     grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
@@ -654,6 +732,27 @@ footer {
         flex: 1 1 auto;
         min-width: 140px;
     }
+
+    .admin-dashboard-actions {
+        flex-direction: column;
+        align-items: stretch;
+    }
+
+    .admin-dashboard-actions > .btn,
+    .admin-dashboard-actions > .admin-actions-dropdown {
+        width: 100%;
+    }
+
+    .admin-actions-dropdown .btn {
+        width: 100%;
+        text-align: center;
+    }
+
+    .admin-actions-menu {
+        position: static;
+        min-width: 0;
+        margin-top: 0.45rem;
+    }
     
     table {
         font-size: 0.9rem;

+ 1 - 0
config.sample.php

@@ -57,6 +57,7 @@ define('DATA_DIR', __DIR__ . '/data/');
 define('PRODUCTS_FILE', DATA_DIR . 'products.json');
 define('RESERVATIONS_FILE', DATA_DIR . 'reservations.json');
 define('ADMINS_FILE', DATA_DIR . 'admins.json');
+define('FAQ_FILE', DATA_DIR . 'faq.json');
 
 // Session settings
 if (session_status() === PHP_SESSION_NONE) {

+ 3 - 0
data/faq.json

@@ -0,0 +1,3 @@
+{
+    "content": "# FAQ\r\n\r\n## Zuschüsse Verein\r\n\r\nErstes T-Shirt ist kostenlos\r\nErstes Poloshirt für 10€\r\n\r\nDie Beträge werden im Shopsystem nicht angepasst, dies bitte bei der Bezahlung klären.\r\n\r\n## Bestellung\r\n\r\nWenn Produkte nicht auf Lager sind, können diese Vorbestellt werden. Du wirst per Mail informiert, wenn ein Vorbestelltes Produkt angekommen ist.\r\n\r\nBestellt wird nach Bedarf, wenn einige Bestellungen wieder zusammengekommen sind.\r\n\r\n## Ansprechpartner\r\n\r\nAnsprechpartner für Produkte und Bestellungen: Bernd Risch\r\nTechnische Probleme: Josef Straßl\r\n"
+}

+ 1 - 0
docs/CONFIG_REFERENCE.md

@@ -36,6 +36,7 @@ It is **not** used for customer login, admin login, password reset, or contact f
 | `PRODUCTS_FILE` | Product data JSON path | Product read/write helpers in `includes/functions.php` |
 | `RESERVATIONS_FILE` | Reservation/backorder JSON path | Reservation read/write helpers in `includes/functions.php` |
 | `ADMINS_FILE` | Admin account JSON path | Admin account read/write helpers in `includes/functions.php` |
+| `FAQ_FILE` | FAQ content JSON path (`content` markdown text) | FAQ read/write and markdown rendering helpers in `includes/functions.php`, pages `faq.php` + `admin/faq.php` |
 
 ## Important notes
 

+ 18 - 0
faq.php

@@ -0,0 +1,18 @@
+<?php
+require_once __DIR__ . '/config.php';
+require_once __DIR__ . '/includes/functions.php';
+
+$pageTitle = 'FAQ';
+$faqContent = getFaqContent();
+$faqHtml = renderFaqMarkdown($faqContent);
+
+include __DIR__ . '/includes/header.php';
+?>
+
+<h2>FAQ</h2>
+
+<div class="panel faq-content">
+    <?php echo $faqHtml; ?>
+</div>
+
+<?php include __DIR__ . '/includes/footer.php'; ?>

+ 146 - 0
includes/functions.php

@@ -268,6 +268,152 @@ function saveProducts($products) {
     writeJsonFile(PRODUCTS_FILE, $data);
 }
 
+/**
+ * Resolve FAQ file path and force storage inside DATA_DIR.
+ */
+function getFaqFilePath(): string {
+    $dataDir = defined('DATA_DIR') && is_string(DATA_DIR) ? DATA_DIR : (dirname(__DIR__) . '/data/');
+    $defaultPath = rtrim($dataDir, '/\\') . '/faq.json';
+
+    if (!defined('FAQ_FILE') || !is_string(FAQ_FILE) || FAQ_FILE === '') {
+        return $defaultPath;
+    }
+
+    $configuredPath = FAQ_FILE;
+    $normalizedDataDir = str_replace('\\', '/', rtrim($dataDir, '/\\')) . '/';
+    $normalizedConfigured = str_replace('\\', '/', $configuredPath);
+
+    if (strpos($normalizedConfigured, $normalizedDataDir) !== 0) {
+        return $defaultPath;
+    }
+
+    return $configuredPath;
+}
+
+/**
+ * Get FAQ markdown content from JSON store.
+ */
+function getFaqContent(): string {
+    $defaultContent = "# FAQ\n\nHier kann der FAQ-Inhalt im Admin-Bereich bearbeitet werden.";
+    $data = readJsonFile(getFaqFilePath());
+    if (!is_array($data)) {
+        return $defaultContent;
+    }
+
+    if (!isset($data['content']) || !is_string($data['content'])) {
+        return $defaultContent;
+    }
+
+    return $data['content'];
+}
+
+/**
+ * Save FAQ markdown content to JSON store.
+ */
+function saveFaqContent(string $markdown): void {
+    writeJsonFile(getFaqFilePath(), ['content' => (string) $markdown]);
+}
+
+/**
+ * Render inline markdown safely (bold + italic).
+ */
+function renderFaqInlineMarkdown(string $text): string {
+    $escaped = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
+
+    // Apply markdown after escaping so raw HTML is never executed.
+    $escaped = preg_replace('/\*\*(.+?)\*\*/s', '<strong>$1</strong>', $escaped);
+    $escaped = preg_replace('/(?<!\*)\*(?!\s)(.+?)(?<!\s)\*(?!\*)/s', '<em>$1</em>', $escaped);
+
+    return $escaped;
+}
+
+/**
+ * Render a minimal safe markdown subset for FAQ content.
+ */
+function renderFaqMarkdown(string $markdown): string {
+    $normalized = str_replace(["\r\n", "\r"], "\n", $markdown);
+    $lines = explode("\n", $normalized);
+    $htmlParts = [];
+    $paragraphLines = [];
+    $listType = '';
+
+    $flushParagraph = function () use (&$paragraphLines, &$htmlParts): void {
+        if (empty($paragraphLines)) {
+            return;
+        }
+
+        $renderedLines = [];
+        foreach ($paragraphLines as $line) {
+            $renderedLines[] = renderFaqInlineMarkdown($line);
+        }
+
+        $htmlParts[] = '<p>' . implode("<br>\n", $renderedLines) . '</p>';
+        $paragraphLines = [];
+    };
+
+    $closeList = function () use (&$listType, &$htmlParts): void {
+        if ($listType === '') {
+            return;
+        }
+
+        $htmlParts[] = '</' . $listType . '>';
+        $listType = '';
+    };
+
+    foreach ($lines as $line) {
+        $line = rtrim($line);
+        $trimmed = trim($line);
+
+        if ($trimmed === '') {
+            $flushParagraph();
+            $closeList();
+            continue;
+        }
+
+        if (preg_match('/^(#{1,3})\s+(.+)$/', $trimmed, $matches) === 1) {
+            $flushParagraph();
+            $closeList();
+            $level = strlen($matches[1]);
+            $htmlParts[] = '<h' . $level . '>' . renderFaqInlineMarkdown($matches[2]) . '</h' . $level . '>';
+            continue;
+        }
+
+        if (preg_match('/^\s*[-*]\s+(.+)$/', $line, $matches) === 1) {
+            $flushParagraph();
+            if ($listType !== 'ul') {
+                $closeList();
+                $listType = 'ul';
+                $htmlParts[] = '<ul>';
+            }
+            $htmlParts[] = '<li>' . renderFaqInlineMarkdown($matches[1]) . '</li>';
+            continue;
+        }
+
+        if (preg_match('/^\s*\d+\.\s+(.+)$/', $line, $matches) === 1) {
+            $flushParagraph();
+            if ($listType !== 'ol') {
+                $closeList();
+                $listType = 'ol';
+                $htmlParts[] = '<ol>';
+            }
+            $htmlParts[] = '<li>' . renderFaqInlineMarkdown($matches[1]) . '</li>';
+            continue;
+        }
+
+        $closeList();
+        $paragraphLines[] = $trimmed;
+    }
+
+    $flushParagraph();
+    $closeList();
+
+    if (empty($htmlParts)) {
+        return '<p>Keine FAQ-Inhalte vorhanden.</p>';
+    }
+
+    return implode("\n", $htmlParts);
+}
+
 /**
  * Get all reservations
  */

+ 1 - 0
includes/header.php

@@ -20,6 +20,7 @@
             <nav class="site-nav">
                 <a href="<?php echo SITE_URL; ?>/index.php">Startseite</a>
                 <a href="<?php echo SITE_URL; ?>/cart.php">Warenkorb</a>
+                <a href="<?php echo SITE_URL; ?>/faq.php">FAQ</a>
                 <a href="<?php echo SITE_URL; ?>/orders.php">Meine Bestellungen</a>
             </nav>
         </div>