custom emoji

This commit is contained in:
Greg Sarjeant 2025-06-13 23:46:33 -04:00
parent 4e711265ff
commit 8f2534568d
9 changed files with 233 additions and 20 deletions

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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'];

View File

@ -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;
}
}
?>

View File

@ -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'],

View File

@ -0,0 +1,29 @@
<?php
// welp this model is overkill
class MoodModel{
// This isn't memory-efficient,
// but I think it'll be fine on this app's scales.
public static function loadAll(): array {
global $db;
$stmt = $db->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);
}
}

View File

@ -16,7 +16,7 @@ echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
href="<?php echo htmlspecialchars($config->baseUrl . $config->basePath)?>feed/rss/" />
<link rel="alternate"
type="text/html"
href=<?php echo htmlspecialchars($config->baseUrl . $config->basePath) ?> />
href="<?php echo htmlspecialchars($config->baseUrl . $config->basePath) ?>" />
<description>My tkr</description>
<language>en-us</language>
<lastBuildDate><?php echo date(DATE_RSS); ?></lastBuildDate>

View File

@ -0,0 +1,47 @@
<?php /** @var ConfigModel $config */ ?>
<?php /** @var array $emojiList */ ?>
<h1>Emoji Management</h1>
<div>
<form action="<?= $config->basePath ?>admin/emoji" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<fieldset>
<legend>Add Emoji</legend>
<div class="fieldset-items">
<label for="emoji">Enter an emoji</label>
<input type="text" id="emoji" name="emoji"
required maxlength="4" minlength="1"
pattern="^[\u{1F000}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{1F900}-\u{1F9FF}\u{1FA70}-\u{1FAFF}]$"
placeholder="Enter an emoji"
>
<label for="emoji-description">Description</label>
<input type="text" id="emoji-description" name="emoji-description"
maxlength="40" minlength="1"
placeholder="describe the mood"
>
<button type="submit" name="action" value="add">Add emoji</button>
</fieldset>
</form>
<?php if (!empty($emojiList)): ?>
<form action="<?= $config->basePath ?>admin/emoji" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<fieldset>
<legend>Delete Emoji</legend>
<div class="fieldset-items">
<?php foreach ($emojiList as $emojiItem): ?>
<div class="emoji-checkbox-item">
<input type="checkbox"
id="delete_emoji_<?= htmlspecialchars($emojiItem['id']) ?>"
name="delete_emoji_ids[]"
value="<?= htmlspecialchars($emojiItem['id']) ?>">
<label for="delete_emoji_<?= htmlspecialchars($emojiItem['id']) ?>">
<span class="emoji-display"><?= htmlspecialchars($emojiItem['emoji']) ?></span>
<span class="emoji-description"><?= htmlspecialchars($emojiItem['description']) ?></span>
</label>
</div>
<?php endforeach; ?>
<button type="submit" name="action" value="delete">Delete selected emoji</button>
</div>
</fieldset>
<?php endif; ?>
</form>
</div>

View File

@ -8,6 +8,7 @@
<?php else: ?>
<a href="<?= $config->basePath ?>admin">admin</a>
<a href="<?= $config->basePath ?>admin/css">css</a>
<a href="<?= $config->basePath ?>admin/emoji">emoji</a>
<a href="<?= $config->basePath ?>logout">logout</a>
<?php endif; ?>
</div>