From c27df3d94622bb41c4321a2227b69097b9b6e6ce Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Thu, 31 Jul 2025 07:36:19 +0200 Subject: [PATCH] wip: 2025-07-31T07:36:19+0200 (1753940179) --- public/about.php | 57 +++++++++++---------- public/assets/js/pages/about.js | 18 +++++-- public/assets/js/pages/contact.js | 19 ++++--- public/contact.php | 3 +- public/search.php | 72 +++++++++++++-------------- public/work/index.php | 2 +- public/work/timeline.php | 54 +++++++++----------- public/work/wip.php | 9 +--- src/Consts.php | 14 ------ src/Database/Models/Coffee/Coffee.php | 19 ++++++- src/Database/Models/Coffee/Stats.php | 32 ------------ src/Database/Models/Search/Search.php | 63 ++++++++++++++++------- src/Database/Models/Work/Action.php | 10 +++- src/Database/Models/Work/Timeline.php | 14 +++++- src/Database/Seeds/vlw.sql | 38 ++++++++++++++ src/Database/Tables/Search/Search.php | 14 +++--- src/Database/Tables/Work/Actions.php | 1 + 17 files changed, 249 insertions(+), 190 deletions(-) delete mode 100644 src/Database/Models/Coffee/Stats.php diff --git a/public/about.php b/public/about.php index 25cca65..9396425 100644 --- a/public/about.php +++ b/public/about.php @@ -1,26 +1,23 @@ total_bytes = array_sum(array_map(fn(Language $language): int => $language->bytes(), parent::all())); + $this->total_bytes = array_sum(array_map(fn(Language $language): int => $language->bytes, parent::all())); } public function percent(Language $language, int $mode = PHP_ROUND_HALF_UP): int { - return round(($language->bytes() / $this->total_bytes) * 100, 0, $mode); + return round(($language->bytes / $this->total_bytes) * 100, 0, $mode); } public function percent_string(Language $language): string { @@ -29,17 +26,25 @@ public function bytes_si_string(Language $language): string { // Calculate factor for unit - $factor = floor((strlen($language->bytes()) - 1) / 3); + $factor = floor((strlen($language->bytes) - 1) / 3); // Divide by radix 10 - $format = $language->bytes() / pow(1000, $factor); + $format = $language->bytes / pow(1000, $factor); - return round($format) . " " . FORGEJO_SI_BYTE_MULTIPLE[$factor]; + return round($format) . " " . SI_BYTE_MULTIPLE[$factor]; } }; - $coffee = new class extends Stats { + $coffee = new class extends Coffee { + public readonly int $count_week; + public readonly int $count_week_average; + + public function __construct() { + $this->count_week = parent::count_week(); + $this->count_week_average = parent::count_week_average(); + } + public function week_average_string(): string { - $diff = $this->week() - $this->week_average(); + $diff = $this->count_week - $this->count_week_average; return match (true) { $diff < 0 => "less than", @@ -65,8 +70,8 @@ - - percent_string($language) ?> id ?>
(bytes() ?> bytes)
+
+ percent_string($language) ?> name ?>
(bytes ?> bytes)
@@ -74,11 +79,11 @@ - @@ -86,8 +91,8 @@ - - percent_string($language) ?> id ?>
(bytes() ?> bytes)
+
+ percent_string($language) ?> name ?>
(bytes ?> bytes)
@@ -101,7 +106,7 @@

Personal

-

One thing that most people know about me is that I like coffee.. lots of coffee. In fact, I've had week() ?> cupweek() === 1 ? "" : "s" ?> of coffee in the last 7 days! That's week_average_string() ?> my average of week_average() ?> per week, impressive! Even though you just read that.. I don't consider myself too much of a coffee snob! As long as it's dark roast and warm, I'm probably happy to have it.

+

One thing that most people know about me is that I like coffee.. lots of coffee. In fact, I've had count_week ?> cupcount_week === 1 ? "" : "s" ?> of coffee in the last 7 days! That's week_average_string() ?> my average of count_week_average ?> per week, impressive! Even though you just read that.. I don't consider myself too much of a coffee snob! As long as it's dark roast and warm, I'm probably happy to have it.

At times, I become a true, amateur, armchair detective for a variety of your typical-nerdy topics that I find interesting and you can bet I spend way more time reading about those things than I will ever have use for in life.

Another silent passion of mine that comes out every few years is building computers and fiddling with networking stuff.

@@ -132,4 +137,4 @@

ISO 8601

digital archiving

- + \ No newline at end of file diff --git a/public/assets/js/pages/about.js b/public/assets/js/pages/about.js index c774d7d..4c6e9e5 100644 --- a/public/assets/js/pages/about.js +++ b/public/assets/js/pages/about.js @@ -1,5 +1,3 @@ -import { Hoverpop } from "/assets/js/modules/Hoverpop.mjs"; - const randomIntFromInterval = (min, max) => { return Math.floor(Math.random() * (max - min + 1) + min); } @@ -64,5 +62,17 @@ const implodeInterests = () => { interestsElement.addEventListener(canHover ? "mouseleave" : "touchend", () => implodeInterests()); } -// Languages stacking bar chart hoverpop -new Hoverpop(document.querySelectorAll("stacked-bar-chart chart-segment")); \ No newline at end of file +// Language bar chart hover tooltip +document.querySelectorAll("stacked-bar-chart chart-segment").forEach(element => { + const tooltipElement = element.querySelector("[data-hover]"); + + element.addEventListener("mouseenter", () => tooltipElement.classList.add("hovering")); + element.addEventListener("mouseleave", () => tooltipElement.classList.remove("hovering")); + + element.addEventListener("mousemove", (event) => { + const x = event.layerX - (tooltipElement.clientWidth / 2); + const y = event.layerY + (tooltipElement.clientHeight - 30); + + tooltipElement.style.setProperty("transform", `translate(${x}px, ${y}px)`); + }); +}); \ No newline at end of file diff --git a/public/assets/js/pages/contact.js b/public/assets/js/pages/contact.js index 45f8a85..286805f 100644 --- a/public/assets/js/pages/contact.js +++ b/public/assets/js/pages/contact.js @@ -1,5 +1,3 @@ -import { Hoverpop } from "/assets/js/modules/Hoverpop.mjs"; - class ContactForm { static STORAGE_KEY = "contact_form_message"; @@ -62,7 +60,16 @@ class ContactForm { form ? (new ContactForm(form)) : ContactForm.removeSavedMessage(); } -// Social links hoverpop -{ - new Hoverpop(document.querySelectorAll("social")); -} \ No newline at end of file +document.querySelectorAll("social").forEach(element => { + const tooltipElement = element.querySelector("[data-hover]"); + + element.addEventListener("mouseenter", () => tooltipElement.classList.add("hovering")); + element.addEventListener("mouseleave", () => tooltipElement.classList.remove("hovering")); + + element.addEventListener("mousemove", (event) => { + const x = event.layerX - (tooltipElement.clientWidth / 2); + const y = event.layerY + tooltipElement.clientHeight; + + tooltipElement.style.setProperty("transform", `translate(${x}px, ${y}px)`); + }); +}); \ No newline at end of file diff --git a/public/contact.php b/public/contact.php index 4def19f..24636aa 100644 --- a/public/contact.php +++ b/public/contact.php @@ -112,5 +112,4 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/public/search.php b/public/search.php index 37bed99..2e95513 100644 --- a/public/search.php +++ b/public/search.php @@ -1,26 +1,22 @@ value] ?? null; - } - - public static function get_category(): ?SearchCategoryEnum { - return SearchCategoryEnum::tryFromName($_GET[SearchTable::CATEGORY->value] ?? ""); - } - - public function search(): array { - return parent::all([SearchTable::QUERY->value => self::get_query()]); + public readonly string $query; + public readonly array $results; + + public function __construct() { + $this->query = $_GET[GET_KEY_QUERY] ?? ""; + $this->results = parent::query($this->query, limit: LIMIT_RESULTS); } } @@ -28,54 +24,54 @@ value, $_GET)): ?> - search()): ?> + results): ?>
-

search()) ?> result(s)

-
- search() as $result): ?> + results as $result): ?>
-
- + query)): default: ?>

0 result(s)

-
@@ -86,14 +82,14 @@
- +

Start typing to search

- +

Almost, type at least two letters to search

@@ -104,7 +100,7 @@
- +

Start typing to search

diff --git a/public/work/index.php b/public/work/index.php index 097590c..786e347 100644 --- a/public/work/index.php +++ b/public/work/index.php @@ -135,7 +135,7 @@

latest projects

- TIMELINE_PREVIEW_LIMIT_COUNT])) ?> +
@@ -149,5 +142,4 @@ -
- \ No newline at end of file +
\ No newline at end of file diff --git a/public/work/wip.php b/public/work/wip.php index c2e4ec6..3bddf13 100644 --- a/public/work/wip.php +++ b/public/work/wip.php @@ -1,10 +1,3 @@ -

Soon, very soon!

@@ -13,6 +6,6 @@
\ No newline at end of file diff --git a/src/Consts.php b/src/Consts.php index 9a06eeb..198b5c3 100644 --- a/src/Consts.php +++ b/src/Consts.php @@ -2,20 +2,6 @@ namespace VLW; - /** - * # Media - * Constants related to media files - */ - const MEDIA_DIR = "/public/assets/media/"; - const ICONS_DIR = MEDIA_DIR . "icons/"; - const DEFAULT_BUTTON_ICON = ICONS_DIR . "chevron.svg"; - - /** - * # Search - * Constants for the search API endpoint - */ - const SEARCH_QUERY_MAX_LENGTH = 2048; - /** * # Timeline * Constants related to the work timeline diff --git a/src/Database/Models/Coffee/Coffee.php b/src/Database/Models/Coffee/Coffee.php index bda2595..2f3eafa 100644 --- a/src/Database/Models/Coffee/Coffee.php +++ b/src/Database/Models/Coffee/Coffee.php @@ -9,11 +9,12 @@ use VLW\Helpers\UUID; use VLW\Database\Database; use VLW\Database\Models\Model; - use VLW\Database\Tables\Coffee\Coffee as CoffeeTable; + use VLW\Database\Tables\Coffee\{Stats, Coffee as CoffeeTable}; require_once VV::root("src/Helpers/UUID.php"); require_once VV::root("src/Database/Database.php"); require_once VV::root("src/Database/Models/Model.php"); + require_once VV::root("src/Database/Tables/Coffee/Stats.php"); require_once VV::root("src/Database/Tables/Coffee/Coffee.php"); class Coffee extends Model { @@ -37,6 +38,22 @@ ); } + final public static function count_week(): int { + return new Database() + ->from(Stats::TABLE) + ->limit(1) + ->select(Stats::COUNT_WEEK->value) + ->fetch_assoc()[Stats::COUNT_WEEK->value] ?? 0; + } + + final public static function count_week_average(): int { + return new Database() + ->from(Stats::TABLE) + ->limit(1) + ->select(Stats::COUNT_WEEK_AVERAGE->value) + ->fetch_assoc()[Stats::COUNT_WEEK_AVERAGE->value] ?? 0; + } + public function __construct(public readonly string $id) { parent::__construct(CoffeeTable::TABLE, CoffeeTable::values(), [ CoffeeTable::ID->value => $this->id diff --git a/src/Database/Models/Coffee/Stats.php b/src/Database/Models/Coffee/Stats.php deleted file mode 100644 index 8e777a7..0000000 --- a/src/Database/Models/Coffee/Stats.php +++ /dev/null @@ -1,32 +0,0 @@ -get(StatsTable::COUNT_WEEK->value) ?? 0; - } - - public function week_average(): int { - return $this->get(StatsTable::COUNT_WEEK_AVERAGE->value) ?? 0; - } - } \ No newline at end of file diff --git a/src/Database/Models/Search/Search.php b/src/Database/Models/Search/Search.php index 61e9578..7e8a8de 100644 --- a/src/Database/Models/Search/Search.php +++ b/src/Database/Models/Search/Search.php @@ -3,40 +3,69 @@ namespace VLW\Database\Models\Search; use \VV; + use \vlw\MySQL\Operators; - use VLW\API\Endpoints; + use VLW\Helpers\UUID; + use VLW\Database\Database; use VLW\Database\Models\Model; - use VLW\Database\Tables\Search\{SearchTable, SearchCategoryEnum}; + use VLW\Database\Tables\Search\{Search as SearchTable, SearchTypeEnum}; - require_once VV::root("src/Consts.php"); - require_once VV::root("src/API/Endpoints.php"); + require_once VV::root("src/Helpers/UUID.php"); + require_once VV::root("src/Database/Database.php"); require_once VV::root("src/Database/Models/Model.php"); require_once VV::root("src/Database/Tables/Search/Search.php"); class Search extends Model { + final public static function new(string $query, SearchTypeEnum $type, string $title): self { + $id = UUID::v4(); + + if (!parent::create(SearchTable::TABLE, [ + SearchTable::ID->value => $id, + SearchTable::QUERY->value => $query, + SearchTable::TYPE->value => $type->name, + SearchTable::TITLE->value => $title, + SearchTable::TEXT->value => null, + SearchTable::HREF->value => null + ])) { throw new Exception("Failed to create Search entity"); } + + return new Search($id); + } + + final public static function query(string $query, ?int $limit = null): array { + return array_map(fn(array $search): Search => new Search($search[SearchTable::ID->value]), new Database() + ->from(SearchTable::TABLE) + ->where([SearchTable::QUERY->value => [ + Operators::LIKE->value => "%{$query}%" + ]]) + ->limit($limit) + ->select(SearchTable::ID->value) + ->fetch_all(MYSQLI_ASSOC) + ); + } + public function __construct(public readonly string $id) { - parent::__construct(Endpoints::SEARCH, [ + parent::__construct(SearchTable::TABLE, SearchTable::values(), [ SearchTable::ID->value => $this->id ]); } - public static function all(array $params = []): array { - return array_map(fn(array $item): Search => new Search($item[SearchTable::ID->value]), parent::list(Endpoints::SEARCH, $params)); + final public string $title { + get => $this->get(SearchTable::TITLE->value); + set (string $title) => $this->set(SearchTable::TITLE->value, $title); } - public function title(): ?string { - return $this->get(SearchTable::TITLE->value); + final public ?string $text { + get => $this->get(SearchTable::TEXT->value); + set (?string $text) => $this->set(SearchTable::TEXT->value, $text); } - public function summary(): ?string { - return $this->get(SearchTable::SUMMARY->value); + final public SearchTypeEnum $type { + get => SearchTypeEnum::fromName($this->get(SearchTable::TYPE->value)); + set (SearchTypeEnum $type) => $this->set(SearchTable::TYPE->value, $type->name); } - public function category(): ?SearchCategoryEnum { - return SearchCategoryEnum::tryFromName($this->get(SearchTable::CATEGORY->value)); - } - - public function href(): ?string { - return $this->get(SearchTable::HREF->value); + final public string $href { + get => $this->get(SearchTable::HREF->value); + set (string $href) => $this->set(SearchTable::HREF->value, $href); } } \ No newline at end of file diff --git a/src/Database/Models/Work/Action.php b/src/Database/Models/Work/Action.php index c570d7f..88203cc 100644 --- a/src/Database/Models/Work/Action.php +++ b/src/Database/Models/Work/Action.php @@ -3,13 +3,16 @@ namespace VLW\Database\Models\Work; use \VV; + use \vlw\MySQL\Order; use VLW\Helpers\UUID; + use VLW\Database\Database; use VLW\Database\Models\Model; use VLW\Database\Models\Work\Work; use VLW\Database\Tables\Work\Actions; require_once VV::root("src/Helpers/UUID.php"); + require_once VV::root("src/Database/Database.php"); require_once VV::root("src/Database/Models/Model.php"); require_once VV::root("src/Database/Models/Work/Work.php"); require_once VV::root("src/Database/Tables/Work/Actions.php"); @@ -34,7 +37,7 @@ return array_map(fn(array $tag): Actions => new Actions($tag[Actions::ID->value]), new Database() ->from(Actions::TABLE) ->where([Actions::REF_WORK_ID->value => $work->id]) - ->order([Actions::LABEL->value => Order::DESC]) + ->order([Actions::ORDER_IDX->value => Order::DESC]) ->select(Actions::ID->value) ->fetch_all(MYSQLI_ASSOC) ); @@ -61,6 +64,11 @@ set (?string $href) => $this->set(Actions::HREF->value, $href); } + final public string $text { + get => $this->get(Actions::TEXT->value); + set (string $text) => $this->set(Actions::TEXT->value, $text); + } + final public ?string $classlist { get => $this->get(Actions::CLASSLIST->value); set (?string $classlist) => $this->set(Actions::CLASSLIST->value, $classlist); diff --git a/src/Database/Models/Work/Timeline.php b/src/Database/Models/Work/Timeline.php index 362f4e1..3fade31 100644 --- a/src/Database/Models/Work/Timeline.php +++ b/src/Database/Models/Work/Timeline.php @@ -5,11 +5,13 @@ use \VV; use VLW\Helpers\UUID; + use VLW\Database\Database; use VLW\Database\Models\Model; use VLW\Database\Models\Work\Work; use VLW\Database\Tables\Work\Timeline as TimelineTable; require_once VV::root("src/Helpers/UUID.php"); + require_once VV::root("src/Database/Database.php"); require_once VV::root("src/Database/Models/Model.php"); require_once VV::root("src/Database/Models/Work/Work.php"); require_once VV::root("src/Database/Tables/Work/Timeline.php"); @@ -29,6 +31,14 @@ return new Timeline($id); } + final public static function all(): array { + return array_map(fn(array $work): Timeline => new Timeline($work[TimelineTable::ID->value]), new Database() + ->from(TimelineTable::TABLE) + ->select(TimelineTable::ID->value) + ->fetch_all(MYSQLI_ASSOC) + ); + } + public function __construct(public readonly string $id) { parent::__construct(TimelineTable::TABLE, TimelineTable::values(), [ TimelineTable::ID->value => $this->id @@ -36,8 +46,8 @@ } final public Work $work { - get => $this->get(WorkTable::REF_WORK_ID->value); - set (Work $work) => $this->set(WorkTable::REF_WORK_ID->value, $work->id); + get => new Work($this->get(TimelineTable::REF_WORK_ID->value)); + set (Work $work) => $this->set(TimelineTable::REF_WORK_ID->value, $work->id); } final public int $year { diff --git a/src/Database/Seeds/vlw.sql b/src/Database/Seeds/vlw.sql index cee7c70..17fe3b5 100644 --- a/src/Database/Seeds/vlw.sql +++ b/src/Database/Seeds/vlw.sql @@ -12,6 +12,30 @@ CREATE TABLE `coffee` ( `id` char(36) NOT NULL, `date_created` datetime NOT NULL DEFAULT current_timestamp() ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +DELIMITER $$ +CREATE TRIGGER `coffee_stats_update` AFTER INSERT ON `coffee` FOR EACH ROW BEGIN + DECLARE count_recent INT; + DECLARE count_average INT; + + DELETE FROM coffee_stats; + + SELECT COUNT(*) INTO count_recent + FROM coffee + WHERE date_created > NOW() - INTERVAL 7 DAY; + + SELECT COUNT(*) / COUNT(DISTINCT YEAR(date_created), WEEK(date_created)) + INTO count_average + FROM coffee; + + INSERT INTO coffee_stats (count_week, count_week_average) VALUES (count_recent, count_average); +END +$$ +DELIMITER ; + +CREATE TABLE `coffee_stats` ( + `count_week` smallint(5) UNSIGNED NOT NULL DEFAULT 0, + `count_week_average` smallint(5) UNSIGNED NOT NULL DEFAULT 0 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; CREATE TABLE `languages` ( `id` char(36) NOT NULL, @@ -26,6 +50,15 @@ CREATE TABLE `messages` ( `date_created` datetime NOT NULL DEFAULT current_timestamp() ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +CREATE TABLE `search` ( + `id` char(36) NOT NULL, + `query` text NOT NULL, + `type` enum('WORK') NOT NULL, + `title` varchar(255) NOT NULL, + `text` text DEFAULT NULL, + `href` varchar(255) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + CREATE TABLE `work` ( `id` char(36) NOT NULL, `namespace` varchar(255) NOT NULL, @@ -39,6 +72,7 @@ CREATE TABLE `work_actions` ( `ref_work_id` char(36) NOT NULL, `order_idx` tinyint(3) UNSIGNED NOT NULL DEFAULT 0, `href` varchar(255) DEFAULT NULL, + `text` varchar(255) NOT NULL, `classlist` varchar(255) DEFAULT NULL, `icon_prepend` varchar(255) DEFAULT NULL, `icon_append` varchar(255) DEFAULT NULL @@ -69,6 +103,10 @@ ALTER TABLE `languages` ALTER TABLE `messages` ADD PRIMARY KEY (`id`); +ALTER TABLE `search` + ADD PRIMARY KEY (`id`); +ALTER TABLE `search` ADD FULLTEXT KEY `query` (`query`); + ALTER TABLE `work` ADD PRIMARY KEY (`id`), ADD UNIQUE KEY `pathname` (`namespace`); diff --git a/src/Database/Tables/Search/Search.php b/src/Database/Tables/Search/Search.php index 5091205..0d613c8 100644 --- a/src/Database/Tables/Search/Search.php +++ b/src/Database/Tables/Search/Search.php @@ -4,7 +4,7 @@ use vlw\xEnum; - enum SearchCategory { + enum SearchTypeEnum { use xEnum; case WORK; @@ -15,10 +15,10 @@ const TABLE = "search"; - case QUERY = "query"; - case ID = "id"; - case TITLE = "title"; - case SUMMARY = "summary"; - case CATEGORY = "category"; - case HREF = "href"; + case ID = "id"; + case QUERY = "query"; + case TYPE = "type"; + case TITLE = "title"; + case TEXT = "text"; + case HREF = "href"; } \ No newline at end of file diff --git a/src/Database/Tables/Work/Actions.php b/src/Database/Tables/Work/Actions.php index 76f431f..0efe6de 100644 --- a/src/Database/Tables/Work/Actions.php +++ b/src/Database/Tables/Work/Actions.php @@ -13,6 +13,7 @@ case REF_WORK_ID = "ref_work_id"; case ORDER_IDX = "order_idx"; case HREF = "href"; + case TEXT = "text"; case CLASSLIST = "classlist"; case ICON_PREPEND = "icon_prepend"; case ICON_APPEND = "icon_append";