Add mood feature.

This commit is contained in:
Greg Sarjeant 2025-05-28 20:32:25 -04:00
parent 5ad15a478d
commit a039026e7e
9 changed files with 490 additions and 19 deletions

View File

@ -38,7 +38,8 @@ function confirm_setup(): void {
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
display_name TEXT NOT NULL,
password_hash TEXT NOT NULL
password_hash TEXT NOT NULL,
mood TEXT NULL
)");
$db->exec("CREATE TABLE IF NOT EXISTS settings (

View File

@ -3,23 +3,29 @@ require_once __DIR__ . '/../bootstrap.php';
confirm_setup();
// Made this a class so it could be more obvious where config settings are coming from.
// Felt too much like magic constants in other files before.
class Config {
// properties and default values
public string $siteTitle = 'My tkr';
public string $siteDescription = '';
public string $basePath = '/';
public int $itemsPerPage = 25;
// load config from sqlite database
public static function load(): self {
$db = get_db();
$stmt = $db->query("SELECT site_title, site_description, base_path, items_per_page FROM settings WHERE id=1");
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$c = new self();
if ($row) {
$c->siteTitle = $row['site_title'];
$c->siteDescription = $row['site_description'];
$c->basePath = $row['base_path'];
$c->itemsPerPage = (int) $row['items_per_page'];
}
return $c;
}
}

304
tkr/lib/emoji.php Normal file
View File

@ -0,0 +1,304 @@
<?php
function get_emojis_with_labels(): array {
return [
'faces' => [
['😀', 'grinning face'],
['😃', 'grinning face with big eyes'],
['😄', 'grinning face with smiling eyes'],
['😁', 'beaming face with smiling eyes'],
['😆', 'grinning squinting face'],
['😅', 'grinning face with sweat'],
['😂', 'face with tears of joy'],
['🤣', 'rolling on the floor laughing'],
['😊', 'smiling face with smiling eyes'],
['😇', 'smiling face with halo'],
['🙂', 'slightly smiling face'],
['🙃', 'upside-down face'],
['😉', 'winking face'],
['😌', 'relieved face'],
['😍', 'smiling face with heart-eyes'],
['🥰', 'smiling face with hearts'],
['😘', 'face blowing a kiss'],
['😗', 'kissing face'],
['😚', 'kissing face with closed eyes'],
['😋', 'face savoring food'],
['😛', 'face with tongue'],
['😜', 'winking face with tongue'],
['😝', 'squinting face with tongue'],
['🤪', 'zany face'],
['🤨', 'face with raised eyebrow'],
],
'gestures' => [
['👋', 'waving hand'],
['🤚', 'raised back of hand'],
['🖐️', 'hand with fingers splayed'],
['✋', 'raised hand'],
['🖖', 'vulcan salute'],
['👌', 'OK hand'],
['🤌', 'pinched fingers'],
['🤏', 'pinching hand'],
['✌️', 'victory hand'],
['🤞', 'crossed fingers'],
['🤟', 'love-you gesture'],
['🤘', 'sign of the horns'],
['🤙', 'call me hand'],
['👈', 'backhand index pointing left'],
['👉', 'backhand index pointing right'],
['👆', 'backhand index pointing up'],
['🖕', 'middle finger'],
['👇', 'backhand index pointing down'],
['☝️', 'index pointing up'],
['👍', 'thumbs up'],
['👎', 'thumbs down'],
['✊', 'raised fist'],
['👊', 'oncoming fist'],
['🤛', 'left-facing fist'],
['🤜', 'right-facing fist'],
],
'nature' => [
['☀️', 'sun'],
['🌤️', 'sun behind small cloud'],
['⛅', 'sun behind cloud'],
['🌥️', 'sun behind large cloud'],
['🌦️', 'sun behind rain cloud'],
['🌧️', 'cloud with rain'],
['🌨️', 'cloud with snow'],
['❄️', 'snowflake'],
['🌩️', 'cloud with lightning'],
['🌪️', 'tornado'],
['🌈', 'rainbow'],
['🔥', 'fire'],
['💧', 'droplet'],
['🌊', 'water wave'],
['🌫️', 'fog'],
['🌬️', 'wind face'],
['🍃', 'leaf fluttering in wind'],
['🍂', 'fallen leaf'],
['🍁', 'maple leaf'],
['🌾', 'sheaf of rice'],
['🌵', 'cactus'],
['🌴', 'palm tree'],
['🌳', 'deciduous tree'],
['🌲', 'evergreen tree'],
['🌸', 'cherry blossom'],
],
'animals' => [
['🐶', 'dog face'],
['🐱', 'cat face'],
['🐭', 'mouse face'],
['🐹', 'hamster face'],
['🐰', 'rabbit face'],
['🦊', 'fox face'],
['🐻', 'bear face'],
['🐼', 'panda face'],
['🐨', 'koala'],
['🐯', 'tiger face'],
['🦁', 'lion face'],
['🐮', 'cow face'],
['🐷', 'pig face'],
['🐸', 'frog face'],
['🐵', 'monkey face'],
['🙈', 'see-no-evil monkey'],
['🙉', 'hear-no-evil monkey'],
['🙊', 'speak-no-evil monkey'],
['🐔', 'chicken'],
['🐧', 'penguin'],
['🐦', 'bird'],
['🐤', 'baby chick'],
['🐣', 'hatching chick'],
['🐺', 'wolf face'],
['🦄', 'unicorn face'],
],
'people' => [
['🧑', 'person'],
['👩', 'woman'],
['👨', 'man'],
['👶', 'baby'],
['👧', 'girl'],
['👦', 'boy'],
['🧒', 'child'],
['👵', 'older woman'],
['👴', 'older man'],
['🧓', 'older adult'],
['👲', 'person with skullcap'],
['🧕', 'woman with headscarf'],
['👳', 'person wearing turban'],
['👮', 'police officer'],
['🕵️', 'detective'],
['👷', 'construction worker'],
['💂', 'guard'],
['👸', 'princess'],
['🤴', 'prince'],
['🦸', 'superhero'],
['🦹', 'supervillain'],
['🧙', 'mage'],
['🧛', 'vampire'],
['🧟', 'zombie'],
['🧞', 'genie'],
],
'emotions' => [
['❤️', 'red heart'],
['🧡', 'orange heart'],
['💛', 'yellow heart'],
['💚', 'green heart'],
['💙', 'blue heart'],
['💜', 'purple heart'],
['🖤', 'black heart'],
['🤍', 'white heart'],
['🤎', 'brown heart'],
['💖', 'sparkling heart'],
['💗', 'growing heart'],
['💓', 'beating heart'],
['💞', 'revolving hearts'],
['💕', 'two hearts'],
['💘', 'heart with arrow'],
['💝', 'heart with ribbon'],
['💔', 'broken heart'],
['❣️', 'heart exclamation'],
['💟', 'heart decoration'],
['💤', 'zzz'],
['🤯', 'exploding head'],
['😱', 'face screaming in fear'],
['🥵', 'hot face'],
['🥶', 'cold face'],
['🤬', 'face with symbols on mouth'],
],
'activities' => [
['🚴', 'person biking'],
['🚵', 'person mountain biking'],
['🏃', 'person running'],
['🏃‍♀️', 'woman running'],
['🏋️', 'person lifting weights'],
['🏊', 'person swimming'],
['🏄', 'person surfing'],
['🚣', 'person rowing boat'],
['🤽', 'person playing water polo'],
['🤾', 'person playing handball'],
['⛹️', 'person bouncing ball'],
['🤸', 'person cartwheeling'],
['🧘', 'person in lotus position'],
['🏇', 'horse racing'],
['🧗', 'person climbing'],
['🏕️', 'camping'],
['🎣', 'fishing pole'],
['⛺', 'tent'],
['🎿', 'skis'],
['🏂', 'snowboarder'],
['🛹', 'skateboard'],
['🛼', 'roller skate'],
['🧺', 'basket'],
['🎯', 'bullseye'],
['🏌️', 'person golfing'],
],
'hobbies' => [
['📚', 'books'],
['📖', 'open book'],
['🎧', 'headphone'],
['🎵', 'musical note'],
['🎶', 'musical notes'],
['🎤', 'microphone'],
['🎼', 'musical score'],
['🎷', 'saxophone'],
['🎸', 'guitar'],
['🎹', 'musical keyboard'],
['🎺', 'trumpet'],
['🎻', 'violin'],
['🪕', 'banjo'],
['✍️', 'writing hand'],
['🖊️', 'pen'],
['📝', 'memo'],
['📷', 'camera'],
['📸', 'camera with flash'],
['🎨', 'artist palette'],
['🧵', 'thread'],
['🧶', 'yarn'],
['🪡', 'sewing needle'],
['📹', 'video camera'],
['🎬', 'clapper board'],
['🧩', 'puzzle piece'],
],
'tech' => [
['💻', 'laptop'],
['🖥️', 'desktop computer'],
['🖨️', 'printer'],
['🖱️', 'computer mouse'],
['⌨️', 'keyboard'],
['📱', 'mobile phone'],
['📲', 'mobile phone with arrow'],
['📞', 'telephone receiver'],
['☎️', 'telephone'],
['📟', 'pager'],
['📠', 'fax machine'],
['🔋', 'battery'],
['🔌', 'electric plug'],
['💽', 'computer disk'],
['💾', 'floppy disk'],
['💿', 'optical disk'],
['📀', 'dvd'],
['🧮', 'abacus'],
['🕹️', 'joystick'],
['📡', 'satellite antenna'],
['🔍', 'magnifying glass tilted left'],
['🔎', 'magnifying glass tilted right'],
['🧭', 'compass'],
['📊', 'bar chart'],
['📈', 'chart increasing'],
],
'travel' => [
['✈️', 'airplane'],
['🛫', 'airplane departure'],
['🛬', 'airplane arrival'],
['🚗', 'automobile'],
['🚕', 'taxi'],
['🚙', 'sport utility vehicle'],
['🚌', 'bus'],
['🚎', 'trolleybus'],
['🏎️', 'racing car'],
['🚓', 'police car'],
['🚑', 'ambulance'],
['🚒', 'fire engine'],
['🚐', 'minibus'],
['🛻', 'pickup truck'],
['🚚', 'delivery truck'],
['🚛', 'articulated lorry'],
['🚜', 'tractor'],
['🚲', 'bicycle'],
['🛴', 'kick scooter'],
['🚨', 'police car light'],
['⛵', 'sailboat'],
['🚤', 'speedboat'],
['🛳️', 'passenger ship'],
['⛴️', 'ferry'],
['🚁', 'helicopter'],
],
'food' => [
['🍎', 'red apple'],
['🍌', 'banana'],
['🍇', 'grapes'],
['🍓', 'strawberry'],
['🍉', 'watermelon'],
['🍍', 'pineapple'],
['🥭', 'mango'],
['🍑', 'peach'],
['🍒', 'cherries'],
['🍅', 'tomato'],
['🥦', 'broccoli'],
['🥕', 'carrot'],
['🌽', 'ear of corn'],
['🥔', 'potato'],
['🍞', 'bread'],
['🥐', 'croissant'],
['🥖', 'baguette bread'],
['🧀', 'cheese wedge'],
['🍕', 'pizza'],
['🍔', 'hamburger'],
['🍟', 'french fries'],
['🌭', 'hot dog'],
['🥪', 'sandwich'],
['🌮', 'taco'],
['🍣', 'sushi'],
],
//'custom' => get_user_emojis($db),
];
}

67
tkr/lib/mood.php Normal file
View File

@ -0,0 +1,67 @@
<?php
require_once __DIR__ . '/../bootstrap.php';
require_once LIB_ROOT . '/config.php';
require LIB_ROOT . '/emoji.php';
function get_mood(): ?string {
$config = Config::load();
$db = get_db();
$stmt = $db->prepare("SELECT mood FROM user WHERE username=?");
$stmt->execute([$_SESSION['username']]);
$row = $stmt->fetch();
return $row['mood'];
}
function save_mood(string $mood): void {
$config = Config::load();
$db = get_db();
$stmt = $db->prepare("UPDATE user SET mood=? WHERE username=?");
$stmt->execute([$mood, $_SESSION['username']]);
header("Location: $config->basePath");
exit;
}
function render_emoji_tabs(?string $selected_emoji = null): string {
$emoji_groups = get_emojis_with_labels();
ob_start();
?>
<?php foreach ($emoji_groups as $group => $emojis): ?>
<fieldset id="<?= htmlspecialchars($group) ?>" class="emoji-tab-content">
<legend><?= ucfirst($group) ?></legend>
<?php foreach ($emojis as [$emoji, $desctiption]): ?>
<label class="emoji-option">
<input
type="radio"
name="mood"
value="<?= htmlspecialchars($emoji) ?>"
aria-label="<?=htmlspecialchars($desctiption ?? 'emoji') ?>"
<?= $emoji === $selected_emoji ? 'checked' : '' ?>
>
<span><?= htmlspecialchars($emoji) ?></span>
</label>
<?php endforeach; ?>
</fieldset>
<?php endforeach;
return ob_get_clean();
}
function render_mood_picker(?string $selected_emoji = null): string {
ob_start();
?>
<form action="set_mood.php" method="post" class="emoji-picker-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<?= render_emoji_tabs($selected_emoji) ?>
<button type="submit">Set the mood</button>
</form>
<?php
return ob_get_clean();
}

54
tkr/public/css/tkr.css Normal file
View File

@ -0,0 +1,54 @@
@charset "UTF-8";
body { font-family: sans-serif; margin: 2em; }
.tick { margin-bottom: 1em; }
.ticktime { color: gray; font-size: 0.9em; }
.ticktext {color: black; font-size: 1.0em; }
.pagination a { margin: 0 5px; text-decoration: none; }
.emoji-tabs {
display: flex;
gap: 0.5em;
margin-bottom: 0.5em;
}
.emoji-tab-button {
text-decoration: none;
font-size: 1.2em;
padding: 0.2em 0.5em;
background: #eee;
border-radius: 0.3em;
}
.emoji-tab-content {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(2em, 1fr));
gap: 0.3em;
margin-bottom: 1em;
}
.emoji-option {
text-align: center;
display: block;
cursor: pointer;
}
.emoji-option input {
display: none;
}
.emoji-option span {
font-size: 1.4em;
display: inline-block;
padding: 0.2em;
border-radius: 0.3em;
}
.emoji-option input:checked + span {
background-color: #ddeeff;
outline: 2px solid #339;
}

View File

@ -3,9 +3,10 @@ require_once __DIR__ . '/../bootstrap.php';
confirm_setup();
require LIB_ROOT . '/config.php';
require_once LIB_ROOT . '/config.php';
require LIB_ROOT . '/session.php';
require LIB_ROOT . '/ticks.php';
require LIB_ROOT . '/mood.php';
$config = Config::load();
@ -19,13 +20,7 @@ $ticks = iterator_to_array(stream_ticks($limit, $offset));
<html>
<head>
<title><?= $config->siteTitle ?></title>
<style>
body { font-family: sans-serif; margin: 2em; }
.tick { margin-bottom: 1em; }
.ticktime { color: gray; font-size: 0.9em; }
.ticktext {color: black; font-size: 1.0em; }
.pagination a { margin: 0 5px; text-decoration: none; }
</style>
<link rel="stylesheet" href="<?= htmlspecialchars($config->basePath) ?>css/tkr.css">
</head>
<body>
<h2><?= $config->siteDescription ?></h2>
@ -55,9 +50,9 @@ $ticks = iterator_to_array(stream_ticks($limit, $offset));
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<label for="tick">What's ticking?</label>
<input name="tick" id="tick" type="text">
<button type="submit">Tick</button>
</form>
<p>Current mood: <?= get_mood() ?> | <a href="<?= $config->basePath ?>set_mood.php">Set your mood</a></p>
<p><a href="<?= $config->basePath ?>logout.php">Logout</a> <?= htmlspecialchars($_SESSION['username']) ?> </p>
<?php endif; ?>
</div>

View File

@ -1,7 +1,9 @@
<?php
require_once __DIR__ . '/../bootstrap.php';
require LIB_ROOT . '/config.php';
confirm_setup();
require_once LIB_ROOT . '/config.php';
require LIB_ROOT . '/session.php';
$config = Config::load();
@ -13,6 +15,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
die('Invalid CSRF token');
}
// TODO: move into session.php
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';

View File

@ -11,20 +11,17 @@ confirm_setup();
if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['tick'])) {
// ensure that the session is valid before proceeding
if (!validateCsrfToken($_POST['csrf_token'])) {
// TODO: maybe redirect to login? Maybe with tick preserved?
die('Invalid CSRF token');
}
} else {
// just go back to the index if it's not a POST
header('Location: index.php');
exit;
// save the tick
save_tick($_POST['tick']);
}
// get the config
$config = Config::load();
// save the tick
save_tick($_POST['tick']);
// go back to the index and show the latest tick
// go back to the index (will show the latest tick if one was sent)
header('Location: ' . $config->basePath);
exit;

44
tkr/public/set_mood.php Normal file
View File

@ -0,0 +1,44 @@
<?php
require_once __DIR__ . '/../bootstrap.php';
confirm_setup();
require_once LIB_ROOT . '/config.php';
require LIB_ROOT . '/session.php';
require LIB_ROOT . '/mood.php';
// get the config
$config = Config::load();
if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['mood'])) {
// ensure that the session is valid before proceeding
if (!validateCsrfToken($_POST['csrf_token'])) {
die('Invalid CSRF token');
}
// set the mood
save_mood($_POST['mood']);
// go back to the index and show the latest tick
header('Location: ' . $config->basePath);
exit;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title><?= $config->siteTitle ?></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="<?= htmlspecialchars($config->basePath) ?>css/tkr.css">
</head>
<body>
<h2>How are you feeling?</h2>
<?= render_mood_picker(); ?>
<a class="back-link" href="<?= htmlspecialchars($config->basePath) ?>">Back to home</a>
</body>
</html>