wip: 2025-08-02T13:19:31+0200 (1754133571)

This commit is contained in:
Victor Westerlund 2025-08-02 13:19:31 +02:00
parent c27df3d946
commit 54c9eb39ba
Signed by: vlw
GPG key ID: D0AD730E1057DFC6
13 changed files with 212 additions and 30 deletions

View file

@ -5,16 +5,22 @@
use Reflect\Rules\{Ruleset, Rules, Type}; use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\API; use VLW\API\API;
use VLW\Helpers\{GenerateTimeline, Forgejo}; use VLW\Helpers\{
Forgejo,
GenerateSearch,
GenerateTimeline
};
require_once Path::root("src/API/API.php"); require_once Path::root("src/API/API.php");
require_once Path::root("src/Helpers/Forgejo.php"); require_once Path::root("src/Helpers/Forgejo.php");
require_once Path::root("src/Helpers/GenerateSearch.php");
require_once Path::root("src/Helpers/GenerateTimeline.php"); require_once Path::root("src/Helpers/GenerateTimeline.php");
enum ServiceEnum: string { enum ServiceEnum: string {
use xEnum; use xEnum;
case ALL = "all"; case ALL = "all";
case SEARCH = "search";
case FORGEJO = "forgejo"; case FORGEJO = "forgejo";
case TIMELINE = "timeline"; case TIMELINE = "timeline";
} }
@ -35,6 +41,9 @@
case ServiceEnum::FORGEJO->value: case ServiceEnum::FORGEJO->value:
return new Response($this->update_forgejo()); return new Response($this->update_forgejo());
case ServiceEnum::SEARCH->value:
return new Response($this->update_search());
case ServiceEnum::TIMELINE->value: case ServiceEnum::TIMELINE->value:
return new Response($this->update_timeline()); return new Response($this->update_timeline());
@ -42,14 +51,19 @@
default: default:
return new Response( return new Response(
$this->update_timeline() && $this->update_timeline() &&
$this->update_search() &&
$this->update_forgejo() $this->update_forgejo()
); );
} }
} }
private function update_timeline(): bool { private function update_timeline(): bool {
return new GenerateTimeline()->generate(); return new GenerateTimeline()->generate();
} }
private function update_search(): bool {
return new GenerateSearch()->generate();
}
private function update_forgejo(): bool { private function update_forgejo(): bool {
return new Forgejo()->update(); return new Forgejo()->update();

View file

@ -37,6 +37,6 @@ document.querySelector("header input[type='search']").addEventListener("input",
clearTimeout(event.target._throttle); clearTimeout(event.target._throttle);
event.target._throttle = setTimeout(() => { event.target._throttle = setTimeout(() => {
// Navigate search-results element on user input // Navigate search-results element on user input
new VV(document.querySelector("search-results")).navigate(`/search?query=${event.target.value}`); new VV(document.querySelector("search-results")).navigate(`/search?q=${event.target.value}`);
}, DEBOUNCE_TIMEOUT_MS); }, DEBOUNCE_TIMEOUT_MS);
}); });

View file

@ -7,8 +7,9 @@
require_once VV::root("src/Database/Tables/Search/Search.php"); require_once VV::root("src/Database/Tables/Search/Search.php");
require_once VV::root("src/Database/Models/Search/Search.php"); require_once VV::root("src/Database/Models/Search/Search.php");
const LIMIT_RESULTS = 10; const GET_KEY_QUERY = "q";
const GET_KEY_QUERY = "q"; const LIMIT_RESULTS = 10;
const MIN_QUERY_LENGTH = 2;
$search = new class extends Search { $search = new class extends Search {
public readonly string $query; public readonly string $query;
@ -16,7 +17,7 @@
public function __construct() { public function __construct() {
$this->query = $_GET[GET_KEY_QUERY] ?? ""; $this->query = $_GET[GET_KEY_QUERY] ?? "";
$this->results = parent::query($this->query, limit: LIMIT_RESULTS); $this->results = strlen($this->query) > MIN_QUERY_LENGTH ? parent::query($this->query, limit: LIMIT_RESULTS) : [];
} }
} }
@ -40,7 +41,7 @@
</form> </form>
</section> </section>
<?php if (array_key_exists(SearchTable::QUERY->value, $_GET)): ?> <?php if (isset($_GET[GET_KEY_QUERY])): ?>
<?php if ($search->results): ?> <?php if ($search->results): ?>
<section class="stats"> <section class="stats">
@ -90,7 +91,7 @@
<?php case 1: ?> <?php case 1: ?>
<section class="center"> <section class="center">
<?= VV::embed("public/assets/media/icons/search.svg") ?> <?= VV::embed("public/assets/media/icons/search.svg") ?>
<p>Almost, type at least two letters to search</p> <p>Almost there, type at least two letters to search</p>
</section> </section>
<?php break; ?> <?php break; ?>

View file

@ -109,7 +109,7 @@
<div class="actions"> <div class="actions">
<?php foreach ($work->actions() as $action): ?> <?php foreach ($work->actions() as $action): ?>
<a href="<?= $action->href ?? "/work/{$work->id}" ?>"><button class="inline <?= implode(" ", $action->classlist) ?>"> <a href="<?= $action->href ?? "/work/{$work->id}" ?>"><button class="inline <?= $action->classlist ?>">
<?php if ($action->icon_prepend): ?> <?php if ($action->icon_prepend): ?>
<?= VV::embed("public/assets/media/icons/" . $action->icon_prepend) ?> <?= VV::embed("public/assets/media/icons/" . $action->icon_prepend) ?>
<?php endif; ?> <?php endif; ?>

@ -1 +1 @@
Subproject commit 59c45d52c1845da6b1b06ec5af2e676e327c014d Subproject commit f8d45950d78a16adc64db920b5dbf59dabce5bca

View file

@ -13,6 +13,8 @@
abstract public string $id { get; } abstract public string $id { get; }
private static Database $_db;
protected readonly Database $db; protected readonly Database $db;
private bool $_resolved = false; private bool $_resolved = false;
private array $_row; private array $_row;
@ -22,7 +24,8 @@
private readonly array $columns, private readonly array $columns,
private readonly array $where private readonly array $where
) { ) {
$this->db = new Database(); // Establish once and reuse Database connection
$this->db = self::$_db ??= new Database();
} }
private ?array $row { private ?array $row {

View file

@ -34,7 +34,7 @@
} }
final public static function from(Work $work): array { final public static function from(Work $work): array {
return array_map(fn(array $tag): Actions => new Actions($tag[Actions::ID->value]), new Database() return array_map(fn(array $tag): Action => new Action($tag[Actions::ID->value]), new Database()
->from(Actions::TABLE) ->from(Actions::TABLE)
->where([Actions::REF_WORK_ID->value => $work->id]) ->where([Actions::REF_WORK_ID->value => $work->id])
->order([Actions::ORDER_IDX->value => Order::DESC]) ->order([Actions::ORDER_IDX->value => Order::DESC])

View file

@ -3,6 +3,7 @@
namespace VLW\Database\Models\Work; namespace VLW\Database\Models\Work;
use \VV; use \VV;
use \vlw\MySQL\Order;
use VLW\Helpers\UUID; use VLW\Helpers\UUID;
use VLW\Database\Database; use VLW\Database\Database;
@ -34,6 +35,11 @@
final public static function all(): array { final public static function all(): array {
return array_map(fn(array $work): Timeline => new Timeline($work[TimelineTable::ID->value]), new Database() return array_map(fn(array $work): Timeline => new Timeline($work[TimelineTable::ID->value]), new Database()
->from(TimelineTable::TABLE) ->from(TimelineTable::TABLE)
->order([
TimelineTable::YEAR->value => Order::DESC,
TimelineTable::MONTH->value => Order::DESC,
TimelineTable::DAY->value => Order::DESC
])
->select(TimelineTable::ID->value) ->select(TimelineTable::ID->value)
->fetch_all(MYSQLI_ASSOC) ->fetch_all(MYSQLI_ASSOC)
); );

View file

@ -0,0 +1,96 @@
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
CREATE TABLE `acl` (
`ref_group` varchar(255) DEFAULT NULL,
`ref_endpoint` varchar(255) NOT NULL,
`method` enum('GET','POST','PUT','PATCH','DELETE') NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `acl` (`ref_group`, `ref_endpoint`, `method`) VALUES
(NULL, 'coffee', 'GET'),
(NULL, 'languages', 'GET'),
(NULL, 'update', 'GET'),
(NULL, 'work', 'GET');
CREATE TABLE `endpoints` (
`id` varchar(255) NOT NULL,
`active` tinyint(1) UNSIGNED NOT NULL DEFAULT 1
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `endpoints` (`id`, `active`) VALUES
('coffee', 1),
('languages', 1),
('update', 1),
('work', 1);
CREATE TABLE `groups` (
`id` varchar(255) NOT NULL,
`active` tinyint(1) UNSIGNED NOT NULL DEFAULT 1,
`date_created` int(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `keys` (
`id` varchar(255) NOT NULL,
`active` tinyint(1) NOT NULL DEFAULT 1,
`ref_user` varchar(255) DEFAULT NULL,
`expires` int(32) DEFAULT NULL,
`created` int(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `rel_users_groups` (
`ref_user` varchar(255) NOT NULL,
`ref_group` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `users` (
`id` varchar(255) NOT NULL,
`active` tinyint(1) NOT NULL DEFAULT 1,
`created` int(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE `acl`
ADD KEY `endpoint` (`ref_endpoint`),
ADD KEY `ref_group` (`ref_group`);
ALTER TABLE `endpoints`
ADD PRIMARY KEY (`id`);
ALTER TABLE `groups`
ADD PRIMARY KEY (`id`);
ALTER TABLE `keys`
ADD PRIMARY KEY (`id`),
ADD KEY `ref_user` (`ref_user`);
ALTER TABLE `rel_users_groups`
ADD KEY `ref_user` (`ref_user`),
ADD KEY `ref_group` (`ref_group`);
ALTER TABLE `users`
ADD PRIMARY KEY (`id`);
ALTER TABLE `acl`
ADD CONSTRAINT `acl_ibfk_1` FOREIGN KEY (`ref_endpoint`) REFERENCES `endpoints` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `acl_ibfk_2` FOREIGN KEY (`ref_group`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `keys`
ADD CONSTRAINT `keys_ibfk_1` FOREIGN KEY (`ref_user`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE `rel_users_groups`
ADD CONSTRAINT `rel_users_groups_ibfk_1` FOREIGN KEY (`ref_user`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `rel_users_groups_ibfk_2` FOREIGN KEY (`ref_group`) REFERENCES `groups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View file

@ -13,21 +13,25 @@ CREATE TABLE `coffee` (
`date_created` datetime NOT NULL DEFAULT current_timestamp() `date_created` datetime NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
DELIMITER $$ DELIMITER $$
CREATE TRIGGER `coffee_stats_update` AFTER INSERT ON `coffee` FOR EACH ROW BEGIN CREATE TRIGGER `coffee_stats_update` AFTER INSERT ON `coffee` FOR EACH ROW BEGIN
DECLARE count_recent INT; DECLARE count_recent INT;
DECLARE count_average INT; DECLARE count_average INT;
DELETE FROM coffee_stats;
DELETE FROM coffee_stats;
SELECT COUNT(*) INTO count_recent
FROM coffee
WHERE date_created > NOW() - INTERVAL 7 DAY; SELECT COUNT(*) INTO count_recent
FROM coffee
SELECT COUNT(*) / COUNT(DISTINCT YEAR(date_created), WEEK(date_created)) WHERE date_created > NOW() - INTERVAL 7 DAY;
INTO count_average
FROM coffee;
SELECT COUNT(*) / COUNT(DISTINCT YEAR(date_created), WEEK(date_created))
INSERT INTO coffee_stats (count_week, count_week_average) VALUES (count_recent, count_average); INTO count_average
FROM coffee;
INSERT INTO coffee_stats (count_week, count_week_average) VALUES (count_recent, count_average);
END END
$$ $$
DELIMITER ; DELIMITER ;
@ -56,7 +60,7 @@ CREATE TABLE `search` (
`type` enum('WORK') NOT NULL, `type` enum('WORK') NOT NULL,
`title` varchar(255) NOT NULL, `title` varchar(255) NOT NULL,
`text` text DEFAULT NULL, `text` text DEFAULT NULL,
`href` varchar(255) NOT NULL `href` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `work` ( CREATE TABLE `work` (

View file

@ -0,0 +1,56 @@
<?php
namespace VLW\Helpers;
use \VV;
use VLW\Database\Database;
use VLW\Database\Models\Work\Work;
use VLW\Database\Models\Search\Search;
use VLW\Database\Tables\Search\{Search as SearchTable, SearchTypeEnum};
require_once VV::root("src/Database/Database.php");
require_once VV::root("src/Database/Models/Work/Work.php");
require_once VV::root("src/Database/Models/Search/Search.php");
require_once VV::root("src/Database/Tables/Search/Search.php");
class GenerateSearch {
private readonly Database $db;
public function __construct() {
$this->db = new Database();
}
public function generate(): bool {
$this->truncate();
return $this->index_work();
}
private function truncate(): bool {
return $this->db->from(SearchTable::TABLE)->delete();
}
private function index_work(): bool {
foreach (Work::all() as $work) {
// Construct a space separated fulltext query string from work entity data
$query = strtolower(implode(" ", [
$work->title,
$work->summary ?? "",
$work->date_created->format("Y"),
$work->date_created->format("n"),
$work->date_created->format("j"),
SearchTypeEnum::WORK->name
]));
$search = Search::new($query, SearchTypeEnum::WORK, $work->title);
if (!$search) { return false; }
$search->text = $work->summary;
// Use href from first Work Action if set or default to "about" page from namespace
$search->href = $work->actions() ? $work->actions()[0]->href : "/work/{$work->namespace}";
}
return true;
}
}

View file

@ -24,8 +24,10 @@
$this->truncate(); $this->truncate();
foreach (Work::all() as $work) { foreach (Work::all() as $work) {
Timeline::new($work); if (!Timeline::new($work)) { return false; };
} }
return true;
} }
private function truncate(): bool { private function truncate(): bool {

@ -1 +1 @@
Subproject commit 1549af5be7723979b6acd5a0eda1e1ac1a70e672 Subproject commit 461b2cc82b268ca09919a3506625957a868a9d27