<?php
include_once "Functions/get_content.php";
include_once "Functions/errorHandler.php";
?>
<?php if (!isset($_SESSION["email"])): { echo "<!-- SESSION OK -->"; } ?>
<!-- ***************** LOGIN FORM ***************** -->
<form method="post" action="index.php?page=cms" class="form-login">
<h2 class="login-title"><?= strtoupper("Login") ?></h2>
<?php if (!empty($loginError)): ?>
<p style="color:red"><?= $loginError ?></p>
<?php endif; ?>
<div class="form-group">
<label for="email">E-Mail <span class="required">*</span></label>
<input
id="email"
name="DATA_email"
placeholder="Enter your email"
type="email"
required
minlength="5"
maxlength="50"
pattern="[a-zA-Z\d._%+-]+@[a-zA-Z\d.-]+\.[a-z]{2,}$"
>
</div>
<div class="form-group">
<label for="password">Password <span class="required">*</span></label>
<input
id="password"
name="DATA_password"
placeholder="Enter your password"
type="password"
required
minlength="4"
maxlength="25"
>
</div>
<button class="btn" type="submit" name="BUTTON_send">Login</button>
</form>
<?php else: ?>
<?php
$editablePages = ['timeline', 't3st'];
// validate page selection
$pageParam = $_GET['DATA_pages'] ?? $editablePages[0];
if (!in_array($pageParam, $editablePages)) {
setError("The selected page <strong>" . htmlspecialchars($pageParam) . "</strong> does not exist.");
$currentPage = $editablePages[0]; // fallback to default page
} else {
$currentPage = $pageParam;
}
// validate filter_type
$allowedTypes = ['', 'header', 'text', 'image'];
$filterType = isset($_GET['filter_type']) && in_array($_GET['filter_type'], $allowedTypes)
? $_GET['filter_type']
: '';
// validate filter_date (YYYY-MM-DD)
$filterDate = '';
if (isset($_GET['filter_date']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['filter_date'])) {
$filterDate = $_GET['filter_date'];
}
// validate sort_order
$allowedSort = ['asc', 'desc'];
$sortOrder = isset($_GET['sort_order']) && in_array($_GET['sort_order'], $allowedSort)
? $_GET['sort_order']
: 'asc';
// now the sanitized variables are safe to use in $blocks below
$blocks = getPageContentBlocks($dbc, $currentPage);
if (empty($blocks)) {
setError("No content found in the datbase for the selected page <strong>" . htmlspecialchars($currentPage) . "</strong>.");
}
// load timeline data from JSON file
$timelineData = json_decode(file_get_contents('./Scripts/timelineData.json'), true);
$blockIdToTimelineDate = [];
foreach ($timelineData as $dateLabel => $arr) {
foreach (['ImageBlockID', 'TitleBlockID', 'TextBlockID'] as $blockKey) {
$blockIndex = $arr[$blockKey];
foreach ($blocks as $block) {
if ($block['position'] == $blockIndex) {
$blockIdToTimelineDate[$block['pk_content']] = $dateLabel;
}
}
}
}
if (isset($_POST['BUTTON_save'])) {
/* echo '<pre>' . print_r($_POST, true) . '</pre>'; */
foreach ($_POST['blocks'] as $blockId => $blockData) {
if (isset($blockData['position'], $blockData['content'])) {
$position = $blockData['position'];
$content = $blockData['content']; // default to existing content
$uploadTime = isset($blocks[$blockId]['uploadTime']) ? $blocks[$blockId]['uploadTime'] : date('Y-m-d H:i:s');
// process image upload only if a file was actually submitted
if (
isset($_FILES['image_uploads']['name'][$blockId]) &&
!empty($_FILES['image_uploads']['name'][$blockId]) &&
$_FILES['image_uploads']['error'][$blockId] === UPLOAD_ERR_OK
) {
$uploadDir = 'Images/';
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
// allow only certain image types
$tmpName = $_FILES['image_uploads']['tmp_name'][$blockId];
$fileName = basename($_FILES['image_uploads']['name'][$blockId]);
$fileType = $_FILES['image_uploads']['type'][$blockId];
$fileExtension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array($fileType, $allowedTypes) || !in_array($fileExtension, $allowedExts)) {
setError("Only JPG, JPEG, PNG, GIF, and WEBP images are allowed for block ID $blockId. Received: $fileType");
continue;
}
// or
// allow only any image type
/* $imageInfo = @getimagesize($tmpName);
if ($imageInfo === false) {
setError("The uploaded file for block ID $blockId is not a valid image.");
continue;
} */
$uniqueFileName = $uploadTime . '_' . $fileName;
$targetPath = $uploadDir . $uniqueFileName;
if (move_uploaded_file($tmpName, $targetPath)) {
$content = $targetPath; // overwrite content with new file path
} else {
setError("Failed to upload image for block ID $blockId.");
continue;
}
}
$sql = "UPDATE pierreNimax_content SET content = ?, position = ?, uploadTime = ? WHERE pk_content = ?";
$stmt = mysqli_prepare($dbc, $sql);
mysqli_stmt_bind_param($stmt, "sssi", $content, $position, $uploadTime, $blockId);
mysqli_stmt_execute($stmt);
setSuccess("Changes saved successfully.");
} else {
setError("Missing position or content for block ID $blockId.");
}
}
}
?>
<div id="cms-heading-container">
<h2 class="cms-header">Content Management System</h2>
<button id="show-guide-btn" title="Show CMS Help" class="hint-button">?</button>
</div>
<?php include_once "Functions/errorDisplay.php"; ?>
<!-- User Guide -->
<dialog id="cms-guide-dialog" class="custom-dialog">
<div class="cms-user-guide-content">
<h3>How to Use the CMS</h3>
<ul>
<li>
<strong>Select a page</strong> from the dropdown ("Timeline", "T3st") to begin editing. The page will reload to show the blocks for your chosen page.
</li>
<li>
<strong>Use the filter options</strong> above the blocks grid to quickly find the blocks you want to edit:
<ul>
<li>
<strong>Block Type:</strong> Filter to show only <code>text</code>, <code>header</code>, or <code>image</code> blocks, or view all types.
</li>
<li>
<strong>Updated After:</strong> Show only blocks that were last updated on or after a specific date.
</li>
<li>
<strong>Sort by Position:</strong> Choose to display blocks in ascending or descending order based on their position number.
</li>
<li>
<strong>Apply Filter:</strong> Click this button to update the block list according to your selected filters.
</li>
<li>
<strong>Reset:</strong> Click this to clear all filters and show all blocks for the selected page.
</li>
</ul>
</li>
<li>
Each page consists of <strong>blocks</strong>:
<ul>
<li><code>text</code>: Editable with a textarea.</li>
<li><code>header</code>: For section titles.</li>
<li><code>image</code>: Upload an image or paste an image URL (e.g., <code>https://students.btshub.lu/image.jpg</code>).</li>
</ul>
</li>
<li>
Each block shows its <strong>type</strong> and <strong>position</strong> (used for ordering).
</li>
<li>
Click <strong>"Save All"</strong> to save your changes to all visible blocks.
</li>
</ul>
<button id="close-guide-btn">Close</button>
</div>
</dialog>
<!-- Filter Blocks -->
<form method="GET" action="index.php" class="cms-filter-form">
<input type="hidden" name="page" value="cms">
<div class="filter-row">
<div class="filter-group">
<label for="page-select">Select page</label>
<select name="DATA_pages" id="page-select">
<option value="timeline" selected>Timeline</option>
<option value="t3st">T3st</option>
</select>
</div>
<div class="filter-group">
<label for="filter-type">Block Type</label>
<select name="filter_type" id="filter-type">
<option value="">All</option>
<option value="header">Header</option>
<option value="text">Text</option>
<option value="image">Image</option>
</select>
</div>
<div class="filter-group">
<label for="filter-date">Updated After</label>
<input type="date" name="filter_date" id="filter-date" value="">
</div>
<div class="filter-group">
<label for="sort-order">Sort by Position</label>
<select name="sort_order" id="sort-order">
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</div>
<div class="filter-actions">
<button type="submit" class="btn filter-btn">Apply Filter</button>
<a href="index.php?page=cms" class="btn reset-btn">Reset</a>
</div>
</div>
</form>
<!-- CMS Editor - Main Container with Grid -->
<div class="main-cms-editor-container">
<!-- TODO: Doesn't work properly yet, contents are not added dynamically in timeline.php -->
<!-- Add New Block Section -->
<!-- <form method="POST" class="cms-form-add-block">
<input type="hidden" name="DATA_pages" value="<?= htmlspecialchars($currentPage) ?>">
<div class="content-block categorized-block new-block">
<h4 class="block-heading">ADD NEW BLOCK</h4>
<div class="block-meta">
<label>Block Type:</label>
<select name="new_block[type]" required>
<option value="">Select type</option>
<option value="header">Header</option>
<option value="text">Text</option>
<option value="image">Image</option>
</select>
</div>
<div class="block-meta">
<label>Position:</label>
<input
type="number"
name="new_block[position]"
min="1"
placeholder="Please write only numbers here"
>
</div>
<div class="block-editor">
<label>Content:</label>
<textarea name="new_block[content]" rows="4" cols="80" placeholder="Enter content or image URL..."></textarea>
</div>
<button type="submit" name="BUTTON_addBlock">Add Block</button>
</div>
</form> -->
<form method="POST" enctype="multipart/form-data" class="cms-blocks-form">
<input type="hidden" name="DATA_pages" value="<?= htmlspecialchars($currentPage) ?>">
<input type="hidden" name="filter_type" value="<?= htmlspecialchars($_GET['filter_type'] ?? '') ?>">
<input type="hidden" name="filter_date" value="<?= htmlspecialchars($_GET['filter_date'] ?? '') ?>">
<input type="hidden" name="sort_order" value="<?= htmlspecialchars($_GET['sort_order'] ?? '') ?>">
<div class="cms-grid">
<?php
$filterType = $_GET['filter_type'] ?? '';
$filterDate = $_GET['filter_date'] ?? '';
$sortOrder = $_GET['sort_order'] ?? 'asc';
// -------------------- FILTERING AND SORTING BLOCKS --------------------
// 1. Filter blocks by type if a type filter is set ("text", "image", "header")
if ($filterType) {
// array_filter will keep only those blocks whose 'blockType' matches the selected filter type
$blocks = array_filter($blocks, function($block) use ($filterType) {
// return true if the block's type matches the filter, false otherwise
return $block['blockType'] === $filterType;
});
}
// 2. Filter blocks by upload date if a date filter is set
if ($filterDate) {
// array_filter will keep only those blocks uploaded on or after the selected date
$blocks = array_filter($blocks, function($block) use ($filterDate) {
// extract the date part (YYYY-MM-DD) from the uploadTime string
// compare it to the filter date; keep the block if it's the same or newer
return substr($block['uploadTime'], 0, 10) >= $filterDate;
});
}
// 3. Sort the blocks by their 'position' field, either ascending or descending
usort($blocks, function($a, $b) use ($sortOrder) {
// if the user selected descending order, compare b to a
/*
<=> spaceship operator, https://www.geeksforgeeks.org/php/php-7-spaceship-operator/
❖ -1 if the value on the left is less than the value on the right
❖ 0 if both values are equal
❖ 1 if the value on the left is greater than the value on the right
*/
if ($sortOrder === 'desc') {
return $b['position'] <=> $a['position'];
}
// otherwise, use ascending order (default)
return $a['position'] <=> $b['position'];
});
?>
<?php foreach ($blocks as $block): ?>
<div class="content-block categorized-block">
<h4 class="block-heading"><?= strtoupper($block['blockType']) ?> BLOCK</h4>
<div class="block-meta">
<div class="block-id">
<strong>Block ID:</strong> <?= htmlspecialchars($block['pk_content']) ?>
</div>
<label>Position:</label>
<input type="number" name="blocks[<?= $block['pk_content'] ?>][position]" value="<?= $block['position'] ?>" class="block-position-input" min="1">
</div>
<div class="block-preview">
<label>Preview:</label>
<div class="preview-content">
<?php if ($block['blockType'] === 'image'): ?>
<img src="<?= htmlspecialchars($block['content']) ?>" alt="Image Preview" class="block-image-preview">
<?php else: ?>
<p><?= nl2br(htmlspecialchars($block['content'])) ?></p>
<?php endif; ?>
</div>
</div>
<div class="block-editor">
<?php if ($block['blockType'] === 'image'): ?>
<input
type="file"
name="image_uploads[<?= $block['pk_content'] ?>]"
accept=".jpg,.jpeg,.png,.gif,.webp"
/>
<small>Current image: <?= htmlspecialchars(basename($block['content'])) ?></small>
<input
type="hidden"
name="blocks[<?= $block['pk_content'] ?>][content]"
value="<?= htmlspecialchars($block['content']) ?>"
>
<?php else: ?>
<textarea name="blocks[<?= $block['pk_content'] ?>][content]" rows="5" cols="80"><?= htmlspecialchars($block['content']) ?></textarea>
<?php endif; ?>
</div>
<div class="block-meta">
<label>Uploaded Time:</label>
<p><?= htmlspecialchars($block['uploadTime']) ?></p>
<?php if (isset($blockIdToTimelineDate[$block['pk_content']])): ?>
<small style="color:#ff541d;">
Belongs to timeline date: <strong><?= htmlspecialchars($blockIdToTimelineDate[$block['pk_content']]) ?></strong>
</small>
<?php else: ?>
<small style="color:#aaa;">
Not linked to a timeline date
</small>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<button type="submit" name="BUTTON_save">Save All</button>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
initAutoFocus();
initGuideDialog();
initPageSelectorAutoSubmit();
initBlockTooltips();
});
function initAutoFocus() {
const firstInput = document.querySelector('textarea, input[type="text"]');
if (firstInput) firstInput.focus();
}
function initGuideDialog() {
const guideDialog = document.getElementById("cms-guide-dialog");
const openBtn = document.getElementById("show-guide-btn");
const closeBtn = document.getElementById("close-guide-btn");
if (!guideDialog || !openBtn || !closeBtn) return;
openBtn.addEventListener("click", () => {
if (typeof guideDialog.showModal === "function") {
guideDialog.showModal();
} else {
alert("Your browser doesn't support the <dialog> element.");
}
});
closeBtn.addEventListener("click", () => guideDialog.close());
}
function initPageSelectorAutoSubmit() {
const pageSelect = document.getElementById("page-select");
if (!pageSelect) return;
pageSelect.addEventListener("change", function () {
this.closest("form").submit();
});
}
function initBlockTooltips() {
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.style.position = 'absolute';
tooltip.style.pointerEvents = 'none';
tooltip.style.opacity = 0;
tooltip.style.transition = 'opacity 0.2s';
document.body.appendChild(tooltip);
const descriptions = {
"HEADER": "Used for section titles or major page headings.",
"TEXT": "A block of paragraph-style content. Markdown and HTML allowed.",
"IMAGE": "Displays an image. Upload one via the upload button.",
"POSITION": "Defines the vertical order of blocks on the page.\nLower values appear higher."
};
const targets = [...document.querySelectorAll(".block-heading"), ...document.querySelectorAll("label")];
targets.forEach(el => {
el.addEventListener("mouseenter", (e) => {
const text = el.textContent.toUpperCase();
for (const key in descriptions) {
if (text.includes(key)) {
// make tooltip content as HTML text and not as text content
tooltip.innerHTML = descriptions[key].replace(/\n/g, "<br>");
tooltip.style.opacity = 1;
}
}
});
el.addEventListener("mousemove", (e) => {
tooltip.style.left = `${e.pageX + 15}px`;
tooltip.style.top = `${e.pageY + 10}px`;
});
el.addEventListener("mouseleave", () => {
tooltip.style.opacity = 0;
});
});
}
</script>
<?php endif; ?>