Refactor models. Handle empty ticks. Prep for CSS upload.

This commit is contained in:
Greg Sarjeant 2025-06-09 14:12:59 -04:00
parent 04e813f32c
commit 3690317206
24 changed files with 99 additions and 71 deletions

View File

@ -8,9 +8,9 @@ Currently very much a work in progress, but it's baically functional.
Deploy the `/tkr` directory to a web server that supports php. It will work either as the root of a (sub)domain (e.g. tky.mydomain.com) or if served from a subdirectory (e.g. mydomain.com/tkr). Deploy the `/tkr` directory to a web server that supports php. It will work either as the root of a (sub)domain (e.g. tky.mydomain.com) or if served from a subdirectory (e.g. mydomain.com/tkr).
If you serve it from a subdirectory, set the value of `$basePath` in `/app/Config.php` to the subdirectory name, excluding the trailing slash (e.g. `/tkr`) If you serve it from a subdirectory, set the value of `$basePath` in `config/init.php` to the subdirectory name, excluding the trailing slash (e.g. `/tkr`)
It provides an rss feed at `/rss` relative to where it's being served (e.g. `/tkr/rss` if served from `/tkr/`). Each rss entry links to an individual post (which I call "ticks"). It provides an rss feed at `/rss` and an atom feed at `/atom` relative to where it's being served (e.g. `/tkr/rss` if served from `/tkr/`). Each rss entry links to an individual post (which I call "ticks").
## Serving ## Serving

View File

@ -169,13 +169,6 @@ label {
gap: 0.5em; gap: 0.5em;
} }
.upload-container {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.fieldset-items { .fieldset-items {
margin-bottom: 14px; margin-bottom: 14px;
display: grid; display: grid;
@ -293,6 +286,12 @@ label {
- Once the width exceeds that (e.g. desktops), it will convert to horizontal alignment - Once the width exceeds that (e.g. desktops), it will convert to horizontal alignment
*/ */
@media (min-width: 600px) { @media (min-width: 600px) {
label {
text-align: right;
padding-top: 10px; /* Match input padding */
margin-bottom: 0;
}
.home-container { .home-container {
grid-template-columns: 1fr 2fr; grid-template-columns: 1fr 2fr;
grid-gap: 2em; grid-gap: 2em;
@ -306,12 +305,6 @@ label {
align-items: start; align-items: start;
} }
label {
text-align: right;
padding-top: 10px; /* Match input padding */
margin-bottom: 0;
}
.file-info { .file-info {
grid-column: 2; /* Align with input column */ grid-column: 2; /* Align with input column */
} }

View File

@ -42,7 +42,7 @@ loadClasses();
// Everything's loaded. Now we can start ticking. // Everything's loaded. Now we can start ticking.
Util::confirm_setup(); Util::confirm_setup();
$config = Config::load(); $config = ConfigModel::load();
Session::start(); Session::start();
Session::generateCsrfToken(); Session::generateCsrfToken();

View File

@ -3,8 +3,8 @@ class AdminController extends Controller {
// GET handler // GET handler
// render the admin page // render the admin page
public function index(){ public function index(){
$config = Config::load(); $config = ConfigModel::load();
$user = User::load(); $user = UserModel::load();
$vars = [ $vars = [
'user' => $user, 'user' => $user,
@ -17,22 +17,22 @@ class AdminController extends Controller {
// POST handler // POST handler
// save updated settings // save updated settings
public function handleSave(){ public function handleSave(){
$config = Config::load(); $config = ConfigModel::load();
if (!Config::isFirstSetup()) { if (!ConfigModel::isFirstSetup()) {
if (!Session::isLoggedIn()){ if (!Session::isLoggedIn()){
header('Location: ' . $config->basePath . '/login'); header('Location: ' . $config->basePath . '/login');
exit; exit;
} }
} }
$user = User::load(); $user = UserModel::load();
// handle form submission // handle form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$errors = []; $errors = [];
// User profile // UserModel profile
$username = trim($_POST['username'] ?? ''); $username = trim($_POST['username'] ?? '');
$displayName = trim($_POST['display_name'] ?? ''); $displayName = trim($_POST['display_name'] ?? '');
$about = trim($_POST['about'] ?? ''); $about = trim($_POST['about'] ?? '');
@ -115,11 +115,15 @@ class AdminController extends Controller {
} }
} }
if (Config::isFirstSetup()){ if (ConfigModel::isFirstSetup()){
Config::completeSetup(); ConfigModel::completeSetup();
} }
header('Location: ' . $config->basePath . '/admin'); header('Location: ' . $config->basePath . '/admin');
exit; exit;
} }
}
private function getCustomCss(){
}
}

View File

@ -1,7 +1,7 @@
<?php <?php
class AuthController extends Controller { class AuthController extends Controller {
function showLogin(?string $error = null){ function showLogin(?string $error = null){
$config = Config::load(); $config = ConfigModel::load();
$csrf_token = Session::getCsrfToken(); $csrf_token = Session::getCsrfToken();
$vars = [ $vars = [
@ -14,7 +14,7 @@ class AuthController extends Controller {
} }
function handleLogin(){ function handleLogin(){
$config = Config::load(); $config = ConfigModel::load();
$error = ''; $error = '';
@ -48,7 +48,7 @@ class AuthController extends Controller {
function handleLogout(){ function handleLogout(){
Session::end(); Session::end();
$config = Config::load(); $config = ConfigModel::load();
header('Location: ' . $config->basePath); header('Location: ' . $config->basePath);
exit; exit;
} }

View File

@ -1,12 +1,12 @@
<?php <?php
class FeedController extends Controller { class FeedController extends Controller {
private Config $config; private ConfigModel $config;
private array $ticks; private array $ticks;
private array $vars; private array $vars;
public function __construct(){ public function __construct(){
$this->config = Config::load(); $this->config = ConfigModel::load();
$this->ticks = iterator_to_array(Tick::streamTicks($this->config->itemsPerPage)); $this->ticks = iterator_to_array(TickModel::streamTicks($this->config->itemsPerPage));
$this->vars = [ $this->vars = [
'config' => $this->config, 'config' => $this->config,
'ticks' => $this->ticks, 'ticks' => $this->ticks,

View File

@ -4,12 +4,12 @@ class HomeController extends Controller {
// renders the homepage view. // renders the homepage view.
public function index(){ public function index(){
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1; $page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$config = Config::load(); $config = ConfigModel::load();
$user = User::load(); $user = UserModel::load();
$limit = $config->itemsPerPage; $limit = $config->itemsPerPage;
$offset = ($page - 1) * $limit; $offset = ($page - 1) * $limit;
$ticks = iterator_to_array(Tick::streamTicks($limit, $offset)); $ticks = iterator_to_array(TickModel::streamTicks($limit, $offset));
$view = new HomeView(); $view = new HomeView();
$tickList = $view->renderTicksSection($config->siteDescription, $ticks, $page, $limit); $tickList = $view->renderTicksSection($config->siteDescription, $ticks, $page, $limit);
@ -34,11 +34,13 @@ class HomeController extends Controller {
} }
// save the tick // save the tick
Tick::save($_POST['tick']); if (trim($_POST['tick'])){
TickModel::save($_POST['tick']);
}
} }
// get the config // get the config
$config = Config::load(); $config = ConfigModel::load();
// redirect to the index (will show the latest tick if one was sent) // redirect to the index (will show the latest tick if one was sent)
header('Location: ' . $config->basePath); header('Location: ' . $config->basePath);

View File

@ -1,8 +1,8 @@
<?php <?php
class MoodController extends Controller { class MoodController extends Controller {
public function index(){ public function index(){
$config = Config::load(); $config = ConfigModel::load();
$user = User::load(); $user = UserModel::load();
$view = new MoodView(); $view = new MoodView();
$moodPicker = $view->render_mood_picker(self::get_emojis_with_labels(), $user->mood); $moodPicker = $view->render_mood_picker(self::get_emojis_with_labels(), $user->mood);
@ -23,8 +23,8 @@
} }
// Get the data we need // Get the data we need
$config = Config::load(); $config = ConfigModel::load();
$user = User::load(); $user = UserModel::load();
$mood = $_POST['mood']; $mood = $_POST['mood'];
// set the mood // set the mood

View File

@ -3,7 +3,7 @@
class TickController extends Controller{ class TickController extends Controller{
// every tick is identified by its timestamp // every tick is identified by its timestamp
public function index(string $year, string $month, string $day, string $hour, string $minute, string $second){ public function index(string $year, string $month, string $day, string $hour, string $minute, string $second){
$model = new Tick(); $model = new TickModel();
$tick = $model->get($year, $month, $day, $hour, $minute, $second); $tick = $model->get($year, $month, $day, $hour, $minute, $second);
$this->render('tick.php', $tick); $this->render('tick.php', $tick);
} }

View File

@ -25,7 +25,7 @@ class Session {
} }
public static function isLoggedIn(): bool { public static function isLoggedIn(): bool {
//echo "User ID set: ". isset($_SESSION['user_id']). "<br/>"; //echo "UserModel ID set: ". isset($_SESSION['user_id']). "<br/>";
//exit; //exit;
return isset($_SESSION['user_id']); return isset($_SESSION['user_id']);
} }

View File

@ -89,7 +89,7 @@ class Util {
// See if there's any data in the tables // See if there's any data in the tables
$user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn(); $user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn();
$settings_count = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn(); $settings_count = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn();
$config = Config::load(); $config = ConfigModel::load();
// If either table has no records and we aren't on /admin // If either table has no records and we aren't on /admin
if ($user_count === 0 || $settings_count === 0){ if ($user_count === 0 || $settings_count === 0){

View File

@ -1,5 +1,5 @@
<?php <?php
class Config { class ConfigModel {
// properties and default values // properties and default values
public string $siteTitle = 'My tkr'; public string $siteTitle = 'My tkr';
public string $siteDescription = ''; public string $siteDescription = '';
@ -41,7 +41,7 @@ class Config {
public function save(): self { public function save(): self {
$db = Util::get_db(); $db = Util::get_db();
if (!Config::isFirstSetup()){ if (!ConfigModel::isFirstSetup()){
$stmt = $db->prepare("UPDATE settings SET site_title=?, site_description=?, base_url=?, base_path=?, items_per_page=? WHERE id=1"); $stmt = $db->prepare("UPDATE settings SET site_title=?, site_description=?, base_url=?, base_path=?, items_per_page=? WHERE id=1");
} else { } else {
$stmt = $db->prepare("INSERT INTO settings (id, site_title, site_description, base_url, base_path, items_per_page) VALUES (1, ?, ?, ?, ?, ?)"); $stmt = $db->prepare("INSERT INTO settings (id, site_title, site_description, base_url, base_path, items_per_page) VALUES (1, ?, ?, ?, ?, ?)");

View File

@ -1,5 +1,5 @@
<?php <?php
class Tick { class TickModel {
// Everything in this class just reads from and writes to the filesystem // Everything in this class just reads from and writes to the filesystem
// It doesn't maintain state, so everything's just a static function // It doesn't maintain state, so everything's just a static function
public static function streamTicks(int $limit, int $offset = 0): Generator { public static function streamTicks(int $limit, int $offset = 0): Generator {
@ -95,7 +95,7 @@ class Tick {
return [ return [
'tickTime' => $tickTime, 'tickTime' => $tickTime,
'tick' => $tick, 'tick' => $tick,
'config' => Config::load(), 'config' => ConfigModel::load(),
]; ];
} }
} }

View File

@ -1,5 +1,5 @@
<?php <?php
class User { class UserModel {
// properties // properties
public string $username = ''; public string $username = '';
public string $displayName = ''; public string $displayName = '';
@ -30,7 +30,7 @@ class User {
public function save(): self { public function save(): self {
$db = Util::get_db(); $db = Util::get_db();
if (!Config::isFirstSetup()){ if (!ConfigModel::isFirstSetup()){
$stmt = $db->prepare("UPDATE user SET username=?, display_name=?, about=?, website=?, mood=? WHERE id=1"); $stmt = $db->prepare("UPDATE user SET username=?, display_name=?, about=?, website=?, mood=? WHERE id=1");
} else { } else {
$stmt = $db->prepare("INSERT INTO user (id, username, display_name, about, website, mood) VALUES (1, ?, ?, ?, ?, ?)"); $stmt = $db->prepare("INSERT INTO user (id, username, display_name, about, website, mood) VALUES (1, ?, ?, ?, ?, ?)");

View File

@ -1,5 +1,5 @@
<?php /** @var Config $config */ ?> <?php /** @var ConfigModel $config */ ?>
<?php /** @var User $user */ ?> <?php /** @var UserModel $user */ ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -13,14 +13,14 @@
<form method="post"> <form 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>User settings</legend> <legend>UserModel 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"
name="username" name="username"
value="<?= $user->username ?>" value="<?= $user->username ?>"
required> required>
<label>Display name <span class=required></span></label> <label>Display name <span class=required>*</span></label>
<input type="text" <input type="text"
name="display_name" name="display_name"
value="<?= $user->displayName ?>" value="<?= $user->displayName ?>"
@ -38,41 +38,70 @@
<fieldset> <fieldset>
<legend>Site settings</legend> <legend>Site settings</legend>
<div class="fieldset-items"> <div class="fieldset-items">
<label>Title <span class=required></span></label> <label>Title <span class=required>*</span></label>
<input type="text" <input type="text"
name="site_title" name="site_title"
value="<?= $config->siteTitle ?>" value="<?= $config->siteTitle ?>"
required> required>
<label>Description <span class=required></span></label> <label>Description <span class=required>*</span></label>
<input type="text" <input type="text"
name="site_description" name="site_description"
value="<?= $config->siteDescription ?>"> value="<?= $config->siteDescription ?>">
<label>Base URL </label> <label>Base URL <span class=required>*</span></label>
<input type="text" <input type="text"
name="base_url" name="base_url"
value="<?= $config->baseUrl ?>" value="<?= $config->baseUrl ?>"
required> required>
<label>Base path <span class=required></span></label> <label>Base path <span class=required>*</span></label>
<input type="text" <input type="text"
name="base_path" name="base_path"
value="<?= $config->basePath ?>" value="<?= $config->basePath ?>"
required> required>
<label>Items per page (max 50) <span class=required></span></label> <label>Items per page (max 50) <span class=required>*</span></label>
<input type="number" <input type="number"
name="items_per_page" name="items_per_page"
value="<?= $config->itemsPerPage ?>" min="1" max="50" value="<?= $config->itemsPerPage ?>" min="1" max="50"
required> required>
</div> </div>
<div class="fieldset-items">
<label for="setCssFile">Set CSS File</label>
<select id="setCssFile" name="css_file">
<option value="">Default</option>
</select>
</div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Change password</legend> <legend>Change password</legend>
<div class="fieldset-items"> <div class="fieldset-items">
<label>New password: </label> <label>New password</label>
<input type="password" name="password"> <input type="password" name="password">
<label>Confirm new password: </label> <label>Confirm new password</label>
<input type="password" name="confirm_password"> <input type="password" name="confirm_password">
</div> </div>
</fieldset> </fieldset>
<fieldset>
<legend>CSS Upload</legend>
<div class="fieldset-items">
<form action="/upload-css" method="post" enctype="multipart/form-data">
<label for="uploadCssFile">Select File to Upload</label>
<input type="file"
id="uploadCssFile"
name="uploadCssFile"
accept=".css">
<div class="file-info">
<strong>File Requirements:</strong><br>
Must be a valid CSS file (.css extension)<br>
Maximum size: 2MB<br>
Will be scanned for malicious content
</div>
<label for="description">Description (optional)</label>
<textarea id="description"
name="description"
placeholder="Describe this CSS file..."></textarea>
<button type="submit" class="upload-btn">Upload CSS File</button>
</form>
</div>
</fieldset>
<button type="submit" class="submit-btn">Save Settings</button> <button type="submit" class="submit-btn">Save Settings</button>
</form> </form>
</div> </div>

View File

@ -1,4 +1,4 @@
<?php /** @var Config $config */ ?> <?php /** @var ConfigModel $config */ ?>
<?php /** @var array $ticks */ ?> <?php /** @var array $ticks */ ?>
<?php <?php
$siteTitle = htmlspecialchars($config->siteTitle); $siteTitle = htmlspecialchars($config->siteTitle);

View File

@ -1,4 +1,4 @@
<?php /** @var Config $config */ ?> <?php /** @var ConfigModel $config */ ?>
<?php /** @var array $ticks */ ?> <?php /** @var array $ticks */ ?>
<?php <?php
// Need to have a little php here because the starting xml tag // Need to have a little php here because the starting xml tag

View File

@ -1,6 +1,6 @@
<?php /** @var bool $isLoggedIn */ ?> <?php /** @var bool $isLoggedIn */ ?>
<?php /** @var Config $config */ ?> <?php /** @var ConfigModel $config */ ?>
<?php /** @var User $user */ ?> <?php /** @var UserModel $user */ ?>
<?php /** @var string $tickList */ ?> <?php /** @var string $tickList */ ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>

View File

@ -1,4 +1,4 @@
<?php /** @var Config $config */ ?> <?php /** @var ConfigModel $config */ ?>
<?php /** @var string $csrf_token */ ?> <?php /** @var string $csrf_token */ ?>
<?php /** @var string $error */ ?> <?php /** @var string $error */ ?>
<!DOCTYPE html> <!DOCTYPE html>

View File

@ -1,4 +1,4 @@
<?php /** @var Config $config */ ?> <?php /** @var ConfigModel $config */ ?>
<?php /** @var string $moodPicker */ ?> <?php /** @var string $moodPicker */ ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>

View File

@ -1,4 +1,4 @@
<?php /** @var Config $config */ ?> <?php /** @var ConfigModel $config */ ?>
<title><?= $config->siteTitle ?></title> <title><?= $config->siteTitle ?></title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">

View File

@ -1,4 +1,4 @@
<?php /** @var Config $config */ ?> <?php /** @var ConfigModel $config */ ?>
<div class="navbar"> <div class="navbar">
<a href="<?= $config->basePath ?>">home</a> <a href="<?= $config->basePath ?>">home</a>
<a href="<?= $config->basePath ?>feed/rss">rss</a> <a href="<?= $config->basePath ?>feed/rss">rss</a>

View File

@ -53,7 +53,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<h1>Lets Set Up Your tkr</h1> <h1>Lets Set Up Your tkr</h1>
<form method="post"> <form method="post">
<h3>User settings</h3> <h3>UserModel settings</h3>
<label>Username: <input type="text" name="username" required></label><br> <label>Username: <input type="text" name="username" required></label><br>
<label>Display name: <input type="text" name="display_name" required></label><br> <label>Display name: <input type="text" name="display_name" required></label><br>
<label>Password: <input type="password" name="password" required></label><br> <label>Password: <input type="password" name="password" required></label><br>

View File

@ -1,4 +1,4 @@
<?php /** @var Config $config */ ?> <?php /** @var ConfigModel $config */ ?>
<?php /** @var Date $tickTime */ ?> <?php /** @var Date $tickTime */ ?>
<?php /** @var string $tick */ ?> <?php /** @var string $tick */ ?>
<!DOCTYPE html> <!DOCTYPE html>