From 8f2534568db132eba4b921e07f157336a247b024 Mon Sep 17 00:00:00 2001 From: Greg Sarjeant <1686767+gsarjeant@users.noreply.github.com> Date: Fri, 13 Jun 2025 23:46:33 -0400 Subject: [PATCH] custom emoji --- config/bootstrap.php | 4 +- public/css/tkr.css | 58 +++++++++-- .../CssController/CssController.php | 12 ++- .../MoodController/MoodController.php | 96 +++++++++++++++++-- src/Framework/Router/Router.php | 4 +- src/Model/MoodModel/MoodModel.php | 29 ++++++ templates/feed/rss.php | 2 +- templates/partials/emoji.php | 47 +++++++++ templates/partials/navbar.php | 1 + 9 files changed, 233 insertions(+), 20 deletions(-) create mode 100644 src/Model/MoodModel/MoodModel.php create mode 100644 templates/partials/emoji.php diff --git a/config/bootstrap.php b/config/bootstrap.php index 7da3234..06da29f 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -158,14 +158,14 @@ function create_tables(): void { // css table $db->exec("CREATE TABLE IF NOT EXISTS css ( id INTEGER PRIMARY KEY, - filename TEXT NOT NULL, + filename TEXT UNIQUE NOT NULL, description TEXT NULL )"); // mood table $db->exec("CREATE TABLE IF NOT EXISTS mood ( id INTEGER PRIMARY KEY, - emoji TEXT NOT NULL, + emoji TEXT UNIQUE NOT NULL, description TEXT NOT NULL )"); } catch (PDOException $e) { diff --git a/public/css/tkr.css b/public/css/tkr.css index 2db66fc..aad05fc 100644 --- a/public/css/tkr.css +++ b/public/css/tkr.css @@ -304,6 +304,58 @@ label.description { outline: 2px solid var(--color-emoji-border); } +.emoji-checkbox-item { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + border: 1px solid var(--color-border-light); + border-radius: 6px; + background-color: var(--color-bg-white); + transition: all 0.2s ease; + margin-bottom: 6px; +} + +.emoji-checkbox-item:hover { + background-color: var(--color-hover-light); + border-color: var(--color-primary); +} + +.emoji-checkbox-item input[type="checkbox"] { + width: auto; + margin: 0; + cursor: pointer; +} + +.emoji-checkbox-item label { + display: flex; + align-items: center; + gap: 10px; + margin: 0; + padding: 0; + font-weight: 400; + cursor: pointer; + flex-grow: 1; + text-align: left; +} + +.emoji-display { + font-size: 1.8em; + min-width: 2em; + text-align: center; +} + +.emoji-description { + flex-grow: 1; + color: var(--color-text-primary); + font-size: 1em; +} + +.emoji-checkbox-item input[type="checkbox"]:focus { + outline: none; + box-shadow: 0 0 0 2px var(--shadow-primary); +} + /* Responsive layout - adjusts from 1 to 2 columns based on screen width - min-width makes the mobile (stacked) view the default @@ -338,10 +390,4 @@ label.description { .file-info { grid-column: 2; } - - .form-row .fieldset-items { - grid-template-columns: 200px 1fr; - gap: 16px; - margin-bottom: 16px; - } } \ No newline at end of file diff --git a/src/Controller/CssController/CssController.php b/src/Controller/CssController/CssController.php index 9209a3e..514ee9f 100644 --- a/src/Controller/CssController/CssController.php +++ b/src/Controller/CssController/CssController.php @@ -56,10 +56,10 @@ class CssController extends Controller { $this->handleUpload(); break; case 'set_theme': - $this->handleSetTheme($config); + $this->handleSetTheme(); break; case 'delete': - $this->handleDelete($config); + $this->handleDelete(); break; } @@ -67,7 +67,9 @@ class CssController extends Controller { exit; } - public function handleDelete(ConfigModel $config): void{ + public function handleDelete(): void{ + global $config; + // Don't try to delete the default theme. if (!$_POST['selectCssFile']){ http_response_code(400); @@ -114,7 +116,9 @@ class CssController extends Controller { $config = $config->save(); } - private function handleSetTheme(ConfigModel $config) { + private function handleSetTheme() { + global $config; + if ($_POST['selectCssFile']){ // Set custom theme $config->cssId = $_POST['selectCssFile']; diff --git a/src/Controller/MoodController/MoodController.php b/src/Controller/MoodController/MoodController.php index be99f2b..422f9d2 100644 --- a/src/Controller/MoodController/MoodController.php +++ b/src/Controller/MoodController/MoodController.php @@ -5,7 +5,7 @@ global $user; $view = new MoodView(); - $moodPicker = $view->render_mood_picker(self::get_emojis_with_labels(), $user->mood); + $moodPicker = $view->render_mood_picker(self::getEmojisWithLabels(), $user->mood); $vars = [ 'config' => $config, @@ -15,7 +15,76 @@ $this->render("mood.php", $vars); } - public function handleMood(){ + // Shows the custom emoji management page + public function showCustomEmoji(){ + global $config; + $emojiList = MoodModel::loadAll(); + + $vars = [ + 'config' => $config, + 'emojiList' => $emojiList, + ]; + + $this->render("emoji.php", $vars); + } + + public function handlePost(): void { + global $config; + + switch ($_POST['action']) { + case 'add': + $emoji = trim($_POST['emoji']); + $description = trim($_POST['emoji-description']); + $this->handleAdd($emoji, $description); + break; + case 'delete': + if (!empty($_POST['delete_emoji_ids'])){ + $this->handleDelete(); + } + break; + } + + header('Location: ' . $config->basePath . 'admin/emoji'); + exit; + } + + public function handleAdd(string $emoji, ?string $description=null): void { + // Validate 1 visible character in the emoji + $charCount = mb_strlen($emoji, 'UTF-8'); + if ($charCount !== 1) { + // TODO - handle error + return; + } + + // Validate the emoji is actually an emoji + $emojiPattern = '/^[\x{1F000}-\x{1F9FF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}\x{1F600}-\x{1F64F}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}\x{1F1E0}-\x{1F1FF}\x{1F900}-\x{1F9FF}\x{1FA70}-\x{1FAFF}]$/u'; + + if (!preg_match($emojiPattern, $emoji)) { + // TODO - handle error + return; + } + + // emojis should have more bytes than characters + $byteCount = strlen($emoji); + if ($byteCount <= 1) { + // TODO - handle error + return; + } + + // It looks like an emoji. Let's add it. + MoodModel::add($emoji, $description); + } + + public function handleDelete(): void { + $ids = $_POST['delete_emoji_ids']; + + if (!empty($ids)) { + $moodModel = new MoodModel(); + $moodModel->delete($ids); + } + } + + public function handleSetMood(){ if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['mood'])) { // ensure that the session is valid before proceeding if (!Session::validateCsrfToken($_POST['csrf_token'])) { @@ -37,8 +106,18 @@ } } - private static function get_emojis_with_labels(): array { - return [ + private static function getEmojisWithLabels(): array { + $customEmoji = MoodModel::loadAll(); + + if (!empty($customEmoji)){ + $custom = []; + + foreach ($customEmoji as $item){ + $custom[] = [$item['emoji'], $item['description']]; + } + } + + $emoji = [ 'faces' => [ ['😀', 'grinning face'], ['😄', 'grinning face with smiling eyes'], @@ -239,9 +318,14 @@ ['🛴', 'kick scooter'], ['⛵', 'sailboat'], ], - - //'custom' => get_user_emojis($db), ]; + + // add custom emoji if there are any + if (isset($custom)){ + $emoji = ['custom' => $custom] + $emoji; + } + + return $emoji; } } ?> \ No newline at end of file diff --git a/src/Framework/Router/Router.php b/src/Framework/Router/Router.php index 4365197..10dea0f 100644 --- a/src/Framework/Router/Router.php +++ b/src/Framework/Router/Router.php @@ -10,13 +10,15 @@ class Router { ['admin', 'AdminController@handleSave', ['POST']], ['admin/css', 'CssController'], ['admin/css', 'CssController@handlePost', ['POST']], + ['admin/emoji', 'MoodController@showCustomEmoji'], + ['admin/emoji', 'MoodController@handlePost', ['POST']], ['feed/rss', 'FeedController@rss'], ['feed/atom', 'FeedController@atom'], ['login', 'AuthController@showLogin'], ['login', 'AuthController@handleLogin', ['POST']], ['logout', 'AuthController@handleLogout', ['GET', 'POST']], ['mood', 'MoodController'], - ['mood', 'MoodController@handleMood', ['POST']], + ['mood', 'MoodController@handleSetMood', ['POST']], ['setup', 'AdminController@showSetup'], ['setup', 'AdminController@handleSetup', ['POST']], ['tick/{y}/{m}/{d}/{h}/{i}/{s}', 'TickController'], diff --git a/src/Model/MoodModel/MoodModel.php b/src/Model/MoodModel/MoodModel.php new file mode 100644 index 0000000..40eb0f0 --- /dev/null +++ b/src/Model/MoodModel/MoodModel.php @@ -0,0 +1,29 @@ +query("SELECT id, emoji, description FROM mood"); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + // I'm not going to support editing emoji. + // It'll just be a delete/readd + public static function add(string $emoji, ?string $description): void{ + global $db; + + $stmt = $db->prepare("INSERT INTO mood (emoji, description) VALUES (?, ?)"); + $stmt->execute([$emoji, $description]); + } + + public static function delete(array $idsToDelete): void{ + global $db; + + $placeholders = rtrim(str_repeat('?,', count($idsToDelete)), ','); + $stmt = $db->prepare("DELETE FROM mood WHERE id IN ($placeholders)"); + $stmt->execute($idsToDelete); + } +} \ No newline at end of file diff --git a/templates/feed/rss.php b/templates/feed/rss.php index 79a6ff3..f902d53 100644 --- a/templates/feed/rss.php +++ b/templates/feed/rss.php @@ -16,7 +16,7 @@ echo '' . "\n"; href="baseUrl . $config->basePath)?>feed/rss/" /> baseUrl . $config->basePath) ?> /> + href="baseUrl . $config->basePath) ?>" /> My tkr en-us diff --git a/templates/partials/emoji.php b/templates/partials/emoji.php new file mode 100644 index 0000000..fa4f9f7 --- /dev/null +++ b/templates/partials/emoji.php @@ -0,0 +1,47 @@ + + +

Emoji Management

+
+
+ +
+ Add Emoji +
+ + + + + +
+
+ +
+ +
+ Delete Emoji +
+ +
+ + +
+ + +
+
+ +
+
diff --git a/templates/partials/navbar.php b/templates/partials/navbar.php index 5e12ef6..ec0d92b 100644 --- a/templates/partials/navbar.php +++ b/templates/partials/navbar.php @@ -8,6 +8,7 @@ admin css + emoji logout \ No newline at end of file