Add about and website to profile. Improve layout.

This commit is contained in:
Greg Sarjeant 2025-05-29 20:48:44 -04:00
parent f3ac806c4b
commit 24bf7067d5
10 changed files with 140 additions and 183 deletions

View File

@ -39,6 +39,8 @@ function confirm_setup(): void {
username TEXT NOT NULL,
display_name TEXT NOT NULL,
password_hash TEXT NOT NULL,
about TEXT NULL,
website TEXT NULL,
mood TEXT NULL
)");

View File

@ -3,7 +3,6 @@ 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'],
@ -26,41 +25,31 @@ function get_emojis_with_labels(): array {
['😜', 'winking face with tongue'],
['😝', 'squinting face with tongue'],
['🤪', 'zany face'],
['🤨', 'face with raised eyebrow'],
['🦸', 'superhero'],
['🦹', 'supervillain'],
['🧙', 'mage'],
['🧛', 'vampire'],
['🧟', 'zombie'],
['🧞', 'genie'],
],
'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'],
@ -72,14 +61,9 @@ function get_emojis_with_labels(): array {
['🌊', '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' => [
@ -98,45 +82,14 @@ function get_emojis_with_labels(): array {
['🐷', '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' => [
'hearts' => [
['❤️', 'red heart'],
['🧡', 'orange heart'],
['💛', 'yellow heart'],
@ -155,49 +108,32 @@ function get_emojis_with_labels(): array {
['💝', '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'],
@ -205,71 +141,14 @@ function get_emojis_with_labels(): array {
['🎻', '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'],
@ -294,10 +173,34 @@ function get_emojis_with_labels(): array {
['🍔', 'hamburger'],
['🍟', 'french fries'],
['🌭', 'hot dog'],
['🥪', 'sandwich'],
['🌮', 'taco'],
['🍣', 'sushi'],
],
'vibes' => [
['💤', 'zzz'],
['🤯', 'exploding head'],
['😱', 'face screaming in fear'],
['🥵', 'hot face'],
['🥶', 'cold face'],
['🤬', 'face with symbols on mouth'],
['🤨', 'face with raised eyebrow'],
],
'tech' => [
['💻', 'laptop'],
['📞', 'telephone receiver'],
['🔋', 'battery'],
['💿', 'optical disk'],
['🕹️', 'joystick'],
['🔍', 'magnifying glass tilted left'],
['📈', 'chart increasing'],
],
'travel' => [
['✈️', 'airplane'],
['🚗', 'automobile'],
['🚕', 'taxi'],
['🚲', 'bicycle'],
['🛴', 'kick scooter'],
['⛵', 'sailboat'],
],
//'custom' => get_user_emojis($db),
];

View File

@ -1,34 +1,28 @@
<?php
require_once __DIR__ . '/../bootstrap.php';
require_once LIB_ROOT . '/config.php';
require_once LIB_ROOT . '/user.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();
$user = User::load();
//$db = get_db();
$stmt = $db->prepare("UPDATE user SET mood=? WHERE username=?");
$stmt->execute([$mood, $_SESSION['username']]);
//$stmt = $db->prepare("UPDATE user SET mood=? WHERE username=?");
//$stmt->execute([$mood, $_SESSION['username']]);
$user->mood = $mood;
$user = $user->save();
header("Location: $config->basePath");
exit;
}
function render_emoji_tabs(): string {
$user = User::load();
$emoji_groups = get_emojis_with_labels();
$selected_emoji = get_mood();
$selected_emoji = $user->mood;
ob_start();
?>

View File

@ -1,20 +1,6 @@
<?php
require_once __DIR__ . '/../bootstrap.php';
function escape_and_linkify_tick(string $tick): string {
// escape dangerous characters, but preserve quotes
$safe = htmlspecialchars($tick, ENT_NOQUOTES | ENT_HTML5, 'UTF-8');
// convert URLs to links
$safe = preg_replace_callback(
'~(https?://[^\s<>"\'()]+)~i',
fn($matches) => '<a href="' . htmlspecialchars($matches[1], ENT_QUOTES, 'UTF-8') . '" target="_blank" rel="noopener noreferrer">' . $matches[1] . '</a>',
$safe
);
return $safe;
}
function save_tick(string $tick): void {
// build the tick path and filename from the current time
$date = new DateTime();

View File

@ -9,6 +9,8 @@ class User {
// properties
public string $username;
public string $displayName;
public string $about;
public string $website;
public string $mood;
// load user settings from sqlite database
@ -16,13 +18,15 @@ class User {
$db = get_db();
// There's only ever one user. I'm just leaning into that.
$stmt = $db->query("SELECT username, display_name, mood FROM user WHERE id=1");
$stmt = $db->query("SELECT username, display_name, about, website, mood FROM user WHERE id=1");
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$u = new self();
if ($row) {
$u->username = $row['username'];
$u->displayName = $row['display_name'];
$u->about = $row['about'] ?? '';
$u->website = $row['website'] ?? '';
$u->mood = $row['mood'];
}
@ -32,8 +36,8 @@ class User {
public function save(): self {
$db = get_db();
$stmt = $db->prepare("UPDATE user SET username=?, display_name=?, mood=? WHERE id=1");
$stmt->execute([$this->username, $this->displayName, $this->mood]);
$stmt = $db->prepare("UPDATE user SET username=?, display_name=?, about=?, website=?, mood=? WHERE id=1");
$stmt->execute([$this->username, $this->displayName, $this->about, $this->website, $this->mood]);
return self::load();
}

15
tkr/lib/util.php Normal file
View File

@ -0,0 +1,15 @@
<?php
function escape_and_linkify(string $text): string {
// escape dangerous characters, but preserve quotes
$safe = htmlspecialchars($text, ENT_NOQUOTES | ENT_HTML5, 'UTF-8');
// convert URLs to links
$safe = preg_replace_callback(
'~(https?://[^\s<>"\'()]+)~i',
fn($matches) => '<a href="' . htmlspecialchars($matches[1], ENT_QUOTES, 'UTF-8') . '" target="_blank" rel="noopener noreferrer">' . $matches[1] . '</a>',
$safe
);
return $safe;
}

View File

@ -22,6 +22,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// User profile
$username = trim($_POST['username'] ?? '');
$displayName = trim($_POST['display_name'] ?? '');
$about = trim($_POST['about'] ?? '');
$website = trim($_POST['website'] ?? '');
// Site settings
$siteTitle = trim($_POST['site_title']) ?? '';
$siteDescription = trim($_POST['site_description']) ?? '';
@ -40,6 +43,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$displayName) {
$errors[] = "Display name is required.";
}
// Make sure the website looks like a URL and starts with a protocol
if ($website) {
if (!filter_var($website, FILTER_VALIDATE_URL)) {
$errors[] = "Please enter a valid URL (including http:// or https://).";
} elseif (!preg_match('/^https?:\/\//i', $website)) {
$errors[] = "URL must start with http:// or https://.";
}
}
// Validate site settings
if (!$siteTitle) {
@ -71,6 +83,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Update user profile
$user->username = $username;
$user->displayName = $displayName;
$user->about = $about;
$user->website = $website;
// Save user profile and reload user from database
$user = $user->save();
@ -101,6 +115,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<legend>User settings</legend>
<label class="admin-option">Username: <input type="text" name="username" value="<?= $user->username ?>" required></label><br>
<label class="admin-option">Display name: <input type="text" name="display_name" value="<?= $user->displayName ?>" required></label><br>
<label class="admin-option">About: <input type="text" name="about" value="<?= $user->about ?>"></label><br>
<label class="admin-option">Website: <input type="text" name="website" value="<?= $user->website ?>"></label><br>
</fieldset>
<fieldset id="site_settings" class="admin-settings-group">
<legend>Site settings</legend>

View File

@ -2,6 +2,28 @@
body { font-family: sans-serif; margin: 2em; }
.flex-container {
display: flex;
}
/* Responsive layout - makes a one column layout instead of a two-column layout */
@media (max-width: 800px) {
.flex-container {
flex-direction: column;
}
}
.profile {
flex-grow: 0;
flex-shrink: 0;
flex-basis: 200px;
order: 1;
}
.ticks {
order: 2;
}
.tick { margin-bottom: 1em; }
.ticktime { color: gray; font-size: 0.9em; }

View File

@ -4,11 +4,14 @@ require_once __DIR__ . '/../bootstrap.php';
confirm_setup();
require_once LIB_ROOT . '/config.php';
require_once LIB_ROOT . '/user.php';
require LIB_ROOT . '/session.php';
require LIB_ROOT . '/ticks.php';
require LIB_ROOT . '/mood.php';
require LIB_ROOT . '/util.php';
$config = Config::load();
// I can get away with this before login because there's only one user.
$user = User::load();
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = $config->itemsPerPage;
@ -20,18 +23,43 @@ $ticks = iterator_to_array(stream_ticks($limit, $offset));
<html>
<head>
<title><?= $config->siteTitle ?></title>
<link rel="stylesheet" href="<?= htmlspecialchars($config->basePath) ?>css/tkr.css">
<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?v=<?= time() ?>">
</head>
<body>
<h2><?= $config->siteDescription ?></h2>
<div class="flex-container">
<div class="profile">
<?php if ($isLoggedIn): ?>
<form class="tickform" action="save_tick.php" method="post">
<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>
<?php endif; ?>
<p>Hi, I'm <?= $user->displayName ?></p>
<p><?= $user->about ?></p>
<p>Website: <?= escape_and_linkify($user->website) ?></p>
<p>Current mood: <?= $user->mood ?></p>
<?php if ($isLoggedIn): ?>
<a href="<?= $config->basePath ?>set_mood.php">Set your mood</a></p>
<p><a href="<?= $config->basePath . '/admin.php' ?>">Admin</a></p>
<p><a href="<?= $config->basePath ?>logout.php">Logout</a> <?= htmlspecialchars($user->username) ?> </p>
<?php else: ?>
<p><a href="<?= $config->basePath ?>login.php">Login</a></p>
<?php endif; ?>
</div>
<div class="ticks">
<?php foreach ($ticks as $tick): ?>
<div class="tick">
<span class="ticktime"><?= htmlspecialchars($tick['timestamp']) ?></span>
<span class="ticktext"><?= escape_and_linkify_tick($tick['tick']) ?></span>
</div>
<div class="tick">
<span class="ticktime"><?= htmlspecialchars($tick['timestamp']) ?></span>
<span class="ticktext"><?= escape_and_linkify($tick['tick']) ?></span>
</div>
<?php endforeach; ?>
</div>
<div class="pagination">
<?php if ($page > 1): ?>
@ -40,20 +68,6 @@ $ticks = iterator_to_array(stream_ticks($limit, $offset));
<?php if (count($ticks) === $limit): ?>
<a href="?page=<?= $page + 1 ?>">Older &raquo;</a>
<?php endif; ?>
</div>
<div>
<?php if (!$isLoggedIn): ?>
<p><a href="<?= $config->basePath ?>login.php">Login</a></p>
<?php else: ?>
<form action="save_tick.php" method="post">
<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 . '/admin.php' ?>">Admin</a> | <a href="<?= $config->basePath ?>logout.php">Logout</a> <?= htmlspecialchars($_SESSION['username']) ?> </p>
<?php endif; ?>
</div>
</body>

View File

@ -4,6 +4,7 @@ require_once __DIR__ . '/../bootstrap.php';
require LIB_ROOT . '/config.php';
require LIB_ROOT . '/session.php';
require LIB_ROOT . '/ticks.php';
require LIB_ROOT . '/util.php';
confirm_setup();