Add flash messages.

This commit is contained in:
Greg Sarjeant 2025-06-16 16:47:35 -04:00
parent 0b4348f14b
commit 856677659e
10 changed files with 156 additions and 18 deletions

View File

@ -38,6 +38,23 @@
/* Shadow colors */ /* Shadow colors */
--shadow-primary: rgba(66, 153, 225, 0.1); --shadow-primary: rgba(66, 153, 225, 0.1);
--shadow-primary-strong: rgba(66, 153, 225, 0.3); --shadow-primary-strong: rgba(66, 153, 225, 0.3);
/* Flash colors */
--color-flash-success: #155724;
--color-flash-success-bg: #d4edda;
--color-flash-success-border-left: #28a745;
--color-flash-error: #721c24;
--color-flash-error-bg: #f8d7da;
--color-flash-error-border-left: #dc3545;
--color-flash-warning: #856404;
--color-flash-warning-bg: #fff3cd;
--color-flash-warning-border-left: #ffc107;
--color-flash-info: #0c5460;
--color-flash-info-bg: #d1ecf1;
--color-flash-info-border-left: #17a2b8;
} }
body { body {
@ -227,6 +244,46 @@ label.description {
gap: 0.5em; gap: 0.5em;
} }
.flash-messages {
background: white;
margin-top: 10px;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.flash-message {
padding: 12px 16px;
margin: 5px 0;
border-radius: 4px;
border-left: 4px solid;
font-weight: 500;
}
.flash-success {
background-color: var(--color-flash-success-bg);
border-left-color: var(--color-flash-success-border-left);
color: var(--color-flash-success);
}
.flash-error {
background-color: var(--color-flash-error-bg);
border-left-color: var(--color-flash-error-border-left);
color: var(--color-flash-error);
}
.flash-warning {
background-color: var(--color-flash-warning-bg);
border-left-color: var(--color-flash-warning-border-left);
color: var(--color-flash-warning);
}
.flash-info {
background-color: var(--color-flash-info-bg);
border-left-color: var(--color-flash-info-border-left);
color: var(--color-flash-info);
}
.fieldset-items { .fieldset-items {
margin-bottom: 14px; margin-bottom: 14px;
display: grid; display: grid;

View File

@ -100,11 +100,11 @@ class AdminController extends Controller {
} }
// If a password was sent, make sure it matches the confirmation // If a password was sent, make sure it matches the confirmation
if ($password && !($password = $confirmPassword)){ if ($password && !($password === $confirmPassword)){
$errors[] = "Passwords do not match"; $errors[] = "Passwords do not match";
} }
// TODO: Actually handle errors // Validation complete
if (empty($errors)) { if (empty($errors)) {
// Update site settings // Update site settings
$config->siteTitle = $siteTitle; $config->siteTitle = $siteTitle;
@ -114,6 +114,7 @@ class AdminController extends Controller {
$config->itemsPerPage = $itemsPerPage; $config->itemsPerPage = $itemsPerPage;
// Save site settings and reload config from database // Save site settings and reload config from database
// TODO - raise and handle exception on failure
$config = $config->save(); $config = $config->save();
// Update user profile // Update user profile
@ -123,19 +124,24 @@ class AdminController extends Controller {
$user->website = $website; $user->website = $website;
// Save user profile and reload user from database // Save user profile and reload user from database
// TODO - raise and handle exception on failure
$user = $user->save(); $user = $user->save();
// Update the password if one was sent // Update the password if one was sent
// TODO - raise and handle exception on failure
if($password){ if($password){
$user->set_password($password); $user->set_password($password);
} }
Session::setFlashMessage('success', 'Settings updated');
} else { } else {
echo implode(",", $errors); foreach($errors as $error){
exit; Session::setFlashMessage('error', $error);
}
} }
} }
header('Location: ' . $config->basePath . 'admin'); header('Location: ' . $_SERVER['PHP_SELF']);
exit; exit;
} }
} }

View File

@ -13,6 +13,15 @@ class Controller {
throw new RuntimeException("Template not found: $childTemplatePath"); throw new RuntimeException("Template not found: $childTemplatePath");
} }
// always check for flash messages and add them if they exist
if (Session::hasFlashMessages()){
$flashMessages = Session::getFlashMessages();
$flashView = new FlashView();
$flashSection = $flashView->renderFlashSection($flashMessages);
$vars['flashSection'] = $flashSection;
}
extract($vars, EXTR_SKIP); extract($vars, EXTR_SKIP);
include $templatePath; include $templatePath;
} }

View File

@ -63,7 +63,8 @@ class CssController extends Controller {
break; break;
} }
header('Location: ' . $config->basePath . 'admin/css'); // redirect after handling to avoid resubmitting form
header('Location: ' . $_SERVER['PHP_SELF']);
exit; exit;
} }
@ -114,6 +115,9 @@ class CssController extends Controller {
// Set the theme back to default // Set the theme back to default
$config->cssId = null; $config->cssId = null;
$config = $config->save(); $config = $config->save();
// Set flash message
Session::setFlashMessage('success', 'Theme ' . $cssFilename . ' deleted.');
} }
private function handleSetTheme() { private function handleSetTheme() {
@ -129,6 +133,9 @@ class CssController extends Controller {
// Update the site theme // Update the site theme
$config = $config->save(); $config = $config->save();
// Set flash message
Session::setFlashMessage('success', 'Theme applied.');
} }
private function handleUpload() { private function handleUpload() {
@ -180,10 +187,13 @@ class CssController extends Controller {
$cssModel = new CssModel(); $cssModel = new CssModel();
$cssModel->save($safeFilename, $description); $cssModel->save($safeFilename, $description);
return true; // Set success flash message
Session::setFlashMessage('success', 'Theme uploaded as ' . $safeFilename);
} catch (Exception $e) { } catch (Exception $e) {
return false; // Set error flash message
// Todo - don't do a global catch like this. Subclass Exception.
Session::setFlashMessage('error', 'Upload exception: ' . $e->getMessage());
} }
} }

View File

@ -34,6 +34,30 @@ class Session {
return self::isLoggedIn() && self::isValidCsrfToken($token); return self::isLoggedIn() && self::isValidCsrfToken($token);
} }
// valid types are:
// - success
// - error
// - info
// - warning
public static function setFlashMessage(string $type, string $message): void {
if (!isset($_SESSION['flash'][$type])){
$_SESSION['flash'][$type] = [];
}
$_SESSION['flash'][$type][] = $message;
}
public static function getFlashMessages(): ?array {
if (isset($_SESSION['flash'])) {
$messages = $_SESSION['flash'];
unset($_SESSION['flash']);
return $messages;
}
}
public static function hasFlashMessages(): bool {
return isset($_SESSION['flash']) && !empty($_SESSION['flash']);
}
public static function end(): void { public static function end(): void {
$_SESSION = []; $_SESSION = [];
session_destroy(); session_destroy();

View File

@ -0,0 +1,21 @@
<?php
class FlashView {
public function renderFlashSection(array $flashMessages): string {
ob_start();
?>
<?php if (count($flashMessages) > 0): ?>
<div class="flash-messages">
<?php foreach ($flashMessages as $type => $messages): ?>
<?php foreach ($messages as $message): ?>
<div class="flash-message flash-<?php echo $type; ?>">
<?php echo htmlspecialchars($message); ?>
</div>
<?php endforeach; ?>
<?php endforeach; ?>
</div>
<?php endif;
return ob_get_clean();
}
}

View File

@ -2,6 +2,7 @@
<?php /** @var ConfigModel $config */ ?> <?php /** @var ConfigModel $config */ ?>
<?php /** @var UserModel $user */ ?> <?php /** @var UserModel $user */ ?>
<?php /** @var string $childTemplateFile */ ?> <?php /** @var string $childTemplateFile */ ?>
<?php /** @var srting $flashSection */ ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -25,6 +26,9 @@
</head> </head>
<body> <body>
<?php include TEMPLATES_DIR . '/partials/navbar.php'?> <?php include TEMPLATES_DIR . '/partials/navbar.php'?>
<?php if( isset($flashSection) && !empty($flashSection) ): ?>
<?php echo $flashSection; ?>
<?php endif; ?>
<?php include TEMPLATES_DIR . '/partials/' . $childTemplateFile?> <?php include TEMPLATES_DIR . '/partials/' . $childTemplateFile?>
</body> </body>
</html> </html>

View File

@ -8,7 +8,7 @@
method="post"> method="post">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<fieldset> <fieldset>
<legend>UserModel settings</legend> <legend>User settings</legend>
<div class="fieldset-items"> <div class="fieldset-items">
<label>Username <span class=required>*</span></label> <label>Username <span class=required>*</span></label>
<input type="text" <input type="text"

View File

@ -18,7 +18,9 @@
maxlength="40" minlength="1" maxlength="40" minlength="1"
placeholder="describe the mood" placeholder="describe the mood"
> >
<div></div>
<button type="submit" name="action" value="add">Add emoji</button> <button type="submit" name="action" value="add">Add emoji</button>
</div>
</fieldset> </fieldset>
</form> </form>
<?php if (!empty($emojiList)): ?> <?php if (!empty($emojiList)): ?>

View File

@ -6,8 +6,13 @@
<p style="color:red"><?= htmlspecialchars($error) ?></p> <p style="color:red"><?= htmlspecialchars($error) ?></p>
<?php endif; ?> <?php endif; ?>
<form method="post" action="<?= $config->basePath ?>login"> <form method="post" action="<?= $config->basePath ?>login">
<div class="fieldset-items">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
<label>Username: <input type="text" name="username" required></label><br> <label for="username">Username:</label>
<label>Password: <input type="password" name="password" required></label><br> <input type="text" id="username" name="username" required>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required></label>
<div></div>
<button type="submit" class="submit-btn">Login</button> <button type="submit" class="submit-btn">Login</button>
</div>
</form> </form>