wip: 2025-07-29T16:12:03+0200 (1753798323)

This commit is contained in:
Victor Westerlund 2025-07-29 16:12:03 +02:00
parent c82989d6fe
commit f398d17f81
Signed by: vlw
GPG key ID: D0AD730E1057DFC6
60 changed files with 565 additions and 1437 deletions

View file

@ -1,20 +1,15 @@
[client_api]
base_url = ""
api_key = ""
verify_peer = true
[client_time_available]
time_zone = "Europe/Stockholm"
available_to_hour = 0;
reply_average_hours = 0;
available_from_hour = 0;
[server_database]
[mariadb]
host = ""
user = ""
pass = ""
db = ""
[server_forgejo]
[config_time_available]
time_zone = "Europe/Stockholm"
available_to_hour = 0;
reply_average_hours = 0;
available_from_hour = 0;
[service_forgejo]
base_url = ""
scan_profiles = ""

22
.gitignore vendored
View file

@ -1,22 +1,2 @@
# Public assets #
#################
public/.well-known
public/assets/js/modules/npm
# Bootstrapping #
#################
vendor
node_modules
.env.ini
# OS generated files #
######################
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db
.directory
.env.ini

6
.gitmodules vendored Normal file
View file

@ -0,0 +1,6 @@
[submodule "reflect"]
path = reflect
url = https://codeberg.org/reflect/reflect
[submodule "vegvisir"]
path = vegvisir
url = https://codeberg.org/vegvisir/vegvisir

45
api/update/GET.php Normal file
View file

@ -0,0 +1,45 @@
<?php
use \vlw\xEnum;
use Reflect\{Response, Path};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\API;
use VLW\Helpers\GenerateTimeline;
require_once Path::root("src/API/API.php");
require_once Path::root("src/Helpers/GenerateTimeline.php");
enum ServiceEnum: string {
use xEnum;
case ALL = "all";
case TIMELINE = "timeline";
}
final class GET_Update extends API {
private const KEY_SERVICE = "service";
public function __construct() {
parent::__construct(new Ruleset()->GET([
new Rules(self::KEY_SERVICE)
->type(Type::ENUM, ServiceEnum::values())
->default(ServiceEnum::ALL->value)
]));
}
public function update_timeline(): bool {
return new GenerateTimeline()->generate();
}
public function main(): Response {
switch ($_GET[self::KEY_SERVICE]) {
case ServiceEnum::TIMELINE->value:
return new Response("OK");
case ServiceEnum::ALL->value:
default:
return new Response($this->update_timeline());
}
}
}

28
api/work/GET.php Normal file
View file

@ -0,0 +1,28 @@
<?php
use Reflect\{Response, Path};
use VLW\API\API;
use VLW\Database\Models\Work\{Work, Tag};
require_once Path::root("src/API/API.php");
require_once Path::root("src/Database/Models/Work/Tag.php");
require_once Path::root("src/Database/Models/Work/Work.php");
final class GET_Work extends API {
private static function entity(Work $work): object {
return (object) [
"tags" => array_map(fn(Tag $tag): string => $tag->label->name, Tag::from($work)),
"actions" => [],
"details" => $work
];
}
public function __construct() {
parent::__construct();
}
public function main(): Response {
return new Response(array_map(fn(Work $work): object => self::entity($work), Work::all()));
}
}

View file

@ -1,6 +1,5 @@
{
"require": {
"reflect/client": "dev-master",
"vlw/mysql": "dev-master",
"vlw/xenum": "dev-master"
},

39
composer.lock generated
View file

@ -4,43 +4,15 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f74f68a452514a9d4dd011cef7648a7f",
"content-hash": "a7ce20d192550ef2d037220b593b5eb9",
"packages": [
{
"name": "reflect/client",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://codeberg.org/reflect/client-php",
"reference": "89a8c041044c8c60cefafc4716d5d61b96c43e06"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"Reflect\\": "src/Reflect/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-only"
],
"authors": [
{
"name": "Victor Westerlund",
"email": "victor.vesterlund@gmail.com"
}
],
"description": "Extendable PHP interface for communicating with Reflect API over HTTP or UNIX sockets",
"time": "2024-04-06T14:55:04+00:00"
},
{
"name": "vlw/mysql",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://codeberg.org/vlw/php-mysql",
"reference": "c64eb96049907da60dc9f237d26aef0e531b0015"
"reference": "0e367f797fa9348408881ed758976f21e8c667e4"
},
"default-branch": true,
"type": "library",
@ -60,7 +32,7 @@
}
],
"description": "Abstraction library for common MySQL/MariaDB DML operations with php-mysqli",
"time": "2025-01-30T09:33:10+00:00"
"time": "2025-07-29T07:46:46+00:00"
},
{
"name": "vlw/xenum",
@ -68,7 +40,7 @@
"source": {
"type": "git",
"url": "https://codeberg.org/vlw/php-xenum",
"reference": "1c997a5574656b88a62f5ee160ee5a6439932a2f"
"reference": "ba3f43a9e2787bf938cfbfcb85ea87e5062df294"
},
"default-branch": true,
"type": "library",
@ -88,14 +60,13 @@
}
],
"description": "PHP eXtended Enums. The missing quality-of-life features from PHP 8+ Enums",
"time": "2024-12-02T10:36:32+00:00"
"time": "2025-05-10T11:28:03+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
"reflect/client": 20,
"vlw/mysql": 20,
"vlw/xenum": 20
},

View file

@ -1,40 +0,0 @@
<?php
use Reflect\Rules\Ruleset;
use Reflect\{Response, Path, Call};
use VLW\API\Endpoints;
use VLW\Database\Database;
use VLW\Database\Tables\About\LanguagesTable;
require_once Path::root("src/API/Endpoints.php");
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/About/Languages.php");
class DELETE_AboutLanguages extends Database {
protected readonly Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->validate_or_exit();
parent::__construct();
}
private function languages(): array {
$resp = (new Call(Endpoints::ABOUT_LANGUAGES->value))->get();
return array_column($resp->output(), LanguagesTable::ID->value);
}
// Delete languages cache file if it exists
public function main(): Response {
$this->db->for(LanguagesTable::NAME);
foreach ($this->languages() as $language){
$this->db->delete([LanguagesTable::ID->value => $language]);
}
return new Response();
}
}

View file

@ -1,43 +0,0 @@
<?php
use vlw\MySQL\Order;
use Reflect\{Response, Path, Call};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\Endpoints;
use VLW\Database\Database;
use VLW\Database\Tables\About\LanguagesTable;
require_once Path::root("src/API/Endpoints.php");
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/About/Languages.php");
class GET_AboutLanguages extends Database {
protected readonly Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(LanguagesTable::ID->value))
->type(Type::STRING)
->min(1)
->max(parent::SIZE_VARCHAR),
(new Rules(LanguagesTable::BYTES->value))
->type(Type::NUMBER)
->min(1)
->max(parent::SIZE_UINT32)
]);
$this->ruleset->validate_or_exit();
parent::__construct();
}
public function main(): Response {
return $this->list(LanguagesTable::NAME, LanguagesTable::values(), [
LanguagesTable::BYTES->value => Order::DESC
]);
}
}

View file

@ -1,38 +0,0 @@
<?php
use vlw\MySQL\Order;
use Reflect\{Response, Path, Call};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\Endpoints;
use VLW\Database\Database;
use VLW\Database\Tables\Coffee\CoffeeTable;
require_once Path::root("src/API/Endpoints.php");
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Coffee/Coffee.php");
class GET_Coffee extends Database {
protected readonly Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(CoffeeTable::ID->value))
->type(Type::NUMBER)
->min(1)
->max(parent::SIZE_UINT32)
]);
$this->ruleset->validate_or_exit();
parent::__construct();
}
public function main(): Response {
return $this->list(CoffeeTable::NAME, CoffeeTable::values(), [
CoffeeTable::ID->value => Order::DESC
]);
}
}

View file

@ -1,38 +0,0 @@
<?php
use Reflect\{Response, Path, Call};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\Endpoints;
use VLW\Database\Database;
use VLW\Database\Tables\Coffee\CoffeeTable;
require_once Path::root("src/API/Endpoints.php");
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Coffee/Coffee.php");
class POST_Coffee extends Database {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(CoffeeTable::ID->value))
->type(Type::NUMBER)
->min(1)
->max(parent::SIZE_UINT32)
->default(time()),
]);
$this->ruleset->validate_or_exit();
parent::__construct();
}
public function main(): Response {
return $this->db->for(CoffeeTable::NAME)->insert($_POST) === true
? new Response(null, 201)
: new Response("Database error", 500);
}
}

View file

@ -1,28 +0,0 @@
<?php
use vlw\MySQL\Order;
use Reflect\{Response, Path, Call};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\Endpoints;
use VLW\Database\Database;
use VLW\Database\Tables\Coffee\StatsTable;
require_once Path::root("src/API/Endpoints.php");
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Coffee/Stats.php");
class GET_CoffeeStats extends Database {
protected readonly Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->validate_or_exit();
parent::__construct();
}
public function main(): Response {
return $this->list(StatsTable::NAME, StatsTable::values());
}
}

View file

@ -1,35 +0,0 @@
<?php
use Reflect\{Response, Path, Call};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\Endpoints;
use VLW\Database\Database;
use VLW\Database\Tables\Coffee\{CoffeeTable, StatsTable};
require_once Path::root("src/API/Endpoints.php");
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Coffee/Stats.php");
require_once Path::root("src/Database/Tables/Coffee/Coffee.php");
class POST_CoffeeStats extends Database {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->validate_or_exit();
parent::__construct();
}
public function main(): Response {
$truncate = $this->db->execute_query("DELETE FROM `" . StatsTable::NAME . "`");
// Add a dummy row to run the MariaDB INSERT AFTER Trigger on the coffee database table
$insert = $this->db->for(CoffeeTable::NAME)->insert([CoffeeTable::ID->value => 0]);
// Remove the dummy row
$remove = $this->db->for(CoffeeTable::NAME)->where([CoffeeTable::ID->value => 0])->delete();
return $truncate && $insert && $remove ? new Response() : new Response("Error", 500);
}
}

View file

@ -1,43 +0,0 @@
<?php
use Reflect\{Response, Path};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\Database\Database;
use VLW\Database\Tables\Messages\MessagesTable;
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Messages/Messages.php");
class POST_Messages extends Database {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(MessagesTable::EMAIL->value))
->type(Type::STRING)
->max(255)
->default(null),
(new Rules(MessagesTable::MESSAGE->value))
->required()
->type(Type::STRING)
->min(1)
->max(parent::SIZE_TEXT)
]);
$this->ruleset->validate_or_exit();
parent::__construct();
}
public function main(): Response {
$_POST[MessagesTable::TIMESTAMP_CREATED->value] = time();
return $this->db->for(MessagesTable::NAME)->insert($_POST) === true
? new Response(null, 201)
: new Response("Failed to send message", 500);
}
}

View file

@ -1,35 +0,0 @@
<?php
use vlw\MySQL\Operators;
use Reflect\{Response, Path};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\Database\Database;
use VLW\Database\Tables\Search\SearchTable;
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Search/Search.php");
class DELETE_Search extends Database {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(SearchTable::ID->value))
->required()
->type(Type::STRING)
->min(2)
->max(parent::SIZE_VARCHAR)
]);
$this->ruleset->validate_or_exit();
parent::__construct();
}
public function main(): Response {
return $this->db->for(SearchTable::NAME)->delete($_POST) === true ? new Response() : new Response("", 500);
}
}

View file

@ -1,85 +0,0 @@
<?php
use vlw\MySQL\Operators;
use Reflect\{Response, Path, Call};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\Endpoints;
use VLW\Database\Database;
use VLW\Database\Tables\Search\{SearchTable, SearchCategoryEnum};
require_once Path::root("src/API/Endpoints.php");
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Search/Search.php");
class GET_Search extends Database {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(SearchTable::QUERY->value))
->type(Type::STRING)
->min(2)
->max(parent::SIZE_VARCHAR)
->default(null),
(new Rules(SearchTable::ID->value))
->type(Type::STRING)
->min(1)
->max(10)
->default(null),
(new Rules(SearchTable::CATEGORY->value))
->type(Type::ENUM, SearchCategoryEnum::names())
->default(null)
]);
$this->ruleset->validate_or_exit();
parent::__construct();
}
private static function get_query(): string {
preg_match_all("/[a-zA-Z0-9]+/", $_GET[SearchTable::QUERY->value], $matches);
return strtolower(implode("", $matches[0]));
}
public function main(): Response {
$result = $this->db->for(SearchTable::NAME);
if ($_GET[SearchTable::ID->value]) {
$result = $result->where([SearchTable::ID->value => $_GET[SearchTable::ID->value]]);
} else if ($_GET[SearchTable::QUERY->value]) {
$query = self::get_query();
$filter = [
SearchTable::QUERY->value => [
Operators::LIKE->value => "%{$query}%"
]
];
if ($_GET[SearchTable::CATEGORY->value]) {
$filter[SearchTable::CATEGORY->value] = $_GET[SearchTable::CATEGORY->value];
}
$result = $result->where($filter);
} else {
new Response([], 400);
}
$result = $result->select([
SearchTable::ID->value,
SearchTable::TITLE->value,
SearchTable::SUMMARY->value,
SearchTable::CATEGORY->value,
SearchTable::HREF->value
]);
return $result->num_rows > 0
? new Response($result->fetch_all(MYSQLI_ASSOC))
: new Response([], 404);
}
}

View file

@ -1,67 +0,0 @@
<?php
use vlw\MySQL\Operators;
use Reflect\{Response, Path, Call};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\Endpoints;
use VLW\Database\Database;
use const VLW\SEARCH_QUERY_MAX_LENGTH;
use VLW\Database\Tables\Search\{SearchTable, SearchCategoryEnum};
use VLW\Database\Tables\Work\{WorkTable, ActionsTable};
require_once Path::root("src/Consts.php");
require_once Path::root("src/API/Endpoints.php");
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Work/Work.php");
require_once Path::root("src/Database/Tables/Work/Actions.php");
require_once Path::root("src/Database/Tables/Search/Search.php");
class POST_Search extends Database {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->validate_or_exit();
parent::__construct();
}
private static function truncate_string(string $input): string {
return substr($input, 0, SEARCH_QUERY_MAX_LENGTH);
}
private static function create_query(string $input): string {
preg_match_all("/[a-zA-Z0-9]+/", $input, $matches);
return self::truncate_string(strtolower(implode("", $matches[0])));
}
private function index_work(): void {
foreach ((new Call(Endpoints::WORK->value))->get()->output() as $result) {
$query = self::create_query(implode("", array_values($result)), 0, SEARCH_QUERY_MAX_LENGTH);
// Get actions related to current result
$actions = (new Call(Endpoints::WORK_ACTIONS->value))->params([
ActionsTable::REF_WORK_ID->value => $result[WorkTable::ID->value]
])->get()->output();
$this->db->for(SearchTable::NAME)->insert([
SearchTable::QUERY->value => $query,
SearchTable::ID->value => crc32($query),
SearchTable::TITLE->value => self::truncate_string($result[WorkTable::TITLE->value]),
SearchTable::SUMMARY->value => self::truncate_string($result[WorkTable::SUMMARY->value]),
SearchTable::CATEGORY->value => SearchCategoryEnum::WORK->name,
// Use first action as link for search result
SearchTable::HREF->value => $actions
? self::truncate_string($actions[0][ActionsTable::HREF->value])
: null
]);
}
}
public function main(): Response {
$this->index_work();
return new Response();
}
}

View file

@ -1,26 +0,0 @@
<?php
use Reflect\{Response, Path, Call};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\Endpoints;
require_once Path::root("src/API/Endpoints.php");
class GET_Update {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->validate_or_exit();
}
// Update all runtime database endpoints
public function main(): Response {
(new Call(Endpoints::SEARCH->value))->post();
(new Call(Endpoints::COFFEE_STATS->value))->post();
(new Call(Endpoints::ABOUT_LANGUAGES->value))->post();
return new Response();
}
}

View file

@ -1,49 +0,0 @@
<?php
use vlw\MySQL\Order;
use Reflect\{Response, Path};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\Database\Database;
use VLW\Database\Tables\Work\WorkTable;
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Work/Work.php");
class GET_Work extends Database {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(WorkTable::ID->value))
->type(Type::STRING)
->min(1)
->max(parent::SIZE_VARCHAR),
(new Rules(WorkTable::TITLE->value))
->type(Type::STRING)
->max(parent::SIZE_VARCHAR),
(new Rules(WorkTable::SUMMARY->value))
->type(Type::STRING)
->max(parent::SIZE_TEXT),
(new Rules(WorkTable::CREATED->value))
->type(Type::STRING)
->min(1)
->max(parent::SIZE_VARCHAR)
]);
$this->ruleset->validate_or_exit();
parent::__construct();
}
public function main(): Response {
return $this->list(WorkTable::NAME, WorkTable::values(), [
WorkTable::CREATED->value => Order::DESC
]);
}
}

View file

@ -1,36 +0,0 @@
<?php
use vlw\MySQL\Order;
use Reflect\{Response, Path};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\Database\Database;
use VLW\Database\Tables\Work\ActionsTable;
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Work/Actions.php");
class GET_WorkActions extends Database {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(ActionsTable::REF_WORK_ID->value))
->type(Type::STRING)
->min(1)
->max(parent::SIZE_VARCHAR)
]);
$this->ruleset->validate_or_exit();
parent::__construct();
}
public function main(): Response {
return $this->list(ActionsTable::NAME, ActionsTable::values(), [
ActionsTable::ORDER_IDX->value => Order::DESC
]);
}
}

View file

@ -1,35 +0,0 @@
<?php
use Reflect\{Response, Path};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\Database\Database;
use VLW\Database\Tables\Work\{TagsTable, TagsLabelEnum};
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Work/Tags.php");
class GET_WorkTags extends Database {
private Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(TagsTable::REF_WORK_ID->value))
->min(1)
->max(parent::SIZE_VARCHAR),
(new Rules(TagsTable::LABEL->value))
->type(Type::ENUM, TagsLabelEnum::names())
]);
$this->ruleset->validate_or_exit();
parent::__construct();
}
public function main(): Response {
return $this->list(TagsTable::NAME, TagsTable::values());
}
}

View file

@ -1,53 +0,0 @@
<?php
use vlw\MySQL\Order;
use Reflect\{Response, Path};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\Database\Database;
use VLW\Database\Tables\Work\TimelineTable;
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Work/Timeline.php");
class GET_WorkTimeline extends Database {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(TimelineTable::REF_WORK_ID->value))
->type(Type::STRING)
->min(1)
->max(parent::SIZE_VARCHAR),
(new Rules(TimelineTable::YEAR->value))
->type(Type::NUMBER)
->min(0)
->max(parent::SIZE_UINT16),
(new Rules(TimelineTable::MONTH->value))
->type(Type::NUMBER)
->min(0)
->max(parent::SIZE_UINT8),
(new Rules(TimelineTable::DAY->value))
->type(Type::NUMBER)
->min(0)
->max(parent::SIZE_UINT8)
]);
$this->ruleset->validate_or_exit();
parent::__construct();
}
public function main(): Response {
return $this->list(TimelineTable::NAME, TimelineTable::values(), [
TimelineTable::YEAR->value => Order::DESC,
TimelineTable::MONTH->value => Order::DESC,
TimelineTable::DAY->value => Order::DESC
]);
}
}

View file

@ -1,10 +0,0 @@
# Install dependencies
composer install --optimize-autoloader
npm install
# (Re)create public NPM modules folder
rm -r public/assets/js/modules/npm
mkdir public/assets/js/modules/npm
# Create link to Elevent MJS from public JS modules folder
ln -sr node_modules/elevent/src/Elevent.mjs public/assets/js/modules/npm/Elevent.mjs

17
package-lock.json generated
View file

@ -1,17 +0,0 @@
{
"name": "vlw.se",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"elevent": "^1.0.2"
}
},
"node_modules/elevent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/elevent/-/elevent-1.0.2.tgz",
"integrity": "sha512-ks5LBUBTg4Bpfmj99OcFAzuDGzBRDEZhTyxmq/Y3RbsdBQ4JCaIUYB0M15OBvBWgIn1BnCo4WCSmw0/YbCJliw=="
}
}
}

View file

@ -1,5 +0,0 @@
{
"dependencies": {
"elevent": "^1.0.2"
}
}

View file

@ -1,5 +1,3 @@
import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
// Click to copy email button
{
const EMAIL_CPY_ANIM_DUR_MSECONDS = 1000;
@ -55,7 +53,7 @@ import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
}, EMAIL_CPY_ANIM_DUR_MSECONDS + 100);
}
new Elevent("click", document.querySelector(".email"), async () => {
document.querySelector(".email").addEventListener("click", async () => {
try {
await navigator.clipboard.writeText("victor@vlw.se");

View file

@ -1,59 +1,42 @@
import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
const DEBOUNCE_TIMEOUT_MS = 100;
const CLASSNAME_SEARCHBOX_ACTIVE = "searchboxActive";
// Set global Vegvisir naviation delay for page transition effect
VV.delay = 100;
// Handle search box open/close buttons
{
// Open search box
new Elevent("click", document.querySelector(".searchbox-open"), () => {
document.querySelector("header").classList.add(CLASSNAME_SEARCHBOX_ACTIVE);
// Select searchbox inner input element
document.querySelector("searchbox input").focus();
});
// Close searchbox
new Elevent("click", document.querySelector(".searchbox-close"), () => {
// Disable search button interaction while animation is running
// This is required to prevent conflicts with the :hover "peak" transformation
const searchButtonElement = document.querySelector("header button.search");
const transformDuration = parseInt(window.getComputedStyle(searchButtonElement).getPropertyValue("--transform-duration"));
searchButtonElement.style.setProperty("pointer-events", "none");
document.querySelector("header").classList.remove(CLASSNAME_SEARCHBOX_ACTIVE);
// Wait for the transform animation to finish
setTimeout(() => searchButtonElement.style.removeProperty("pointer-events"), transformDuration);
});
}
// Root shell navigation event handlers
{
// On all top shell navigations
new Elevent(VV.EVENT.START, VV.shell, () => {
// Close searchbox on top shell navigations
document.querySelector("header").classList.remove(CLASSNAME_SEARCHBOX_ACTIVE);
});
}
// Handle search logic
{
const searchResultsElement = document.querySelector("search-results");
document.querySelector("header input[type='search']").addEventListener("input", (event) => {
// Debounce user input
clearTimeout(event.target._throttle);
event.target._throttle = setTimeout(() => {
// Navigate search-results element on user input
new VV(searchResultsElement).navigate(`/search?query=${event.target.value}`);
}, 100);
});
}
// Navigate to the start page if the logo in the header is clicked
document.querySelector("header .logo").addEventListener("click", () => new VV().navigate("/"));
// Scroll page to top on navigation
VV.shell.addEventListener(VV.EVENT.FINISH, () => window.scrollTo({ top: 0 }));
VV.shell.addEventListener(VV.EVENT.FINISH, () => window.scrollTo({ top: 0 }));
// Open search box
document.querySelector(".searchbox-open").addEventListener("click", () => {
document.querySelector("header").classList.add(CLASSNAME_SEARCHBOX_ACTIVE);
// Select searchbox inner input element
document.querySelector("searchbox input").focus();
});
// Close searchbox
document.querySelector(".searchbox-close").addEventListener("click", () => {
// Disable search button interaction while animation is running
// This is required to prevent conflicts with the :hover "peak" transformation
const searchButtonElement = document.querySelector("header button.search");
const transformDuration = parseInt(window.getComputedStyle(searchButtonElement).getPropertyValue("--transform-duration"));
searchButtonElement.style.setProperty("pointer-events", "none");
document.querySelector("header").classList.remove(CLASSNAME_SEARCHBOX_ACTIVE);
// Wait for the transform animation to finish
setTimeout(() => searchButtonElement.style.removeProperty("pointer-events"), transformDuration);
});
// Close searchbox on top shell navigations
VV.shell.addEventListener(VV.EVENT.START, () => document.querySelector("header").classList.remove(CLASSNAME_SEARCHBOX_ACTIVE));
// Handle search logic
document.querySelector("header input[type='search']").addEventListener("input", (event) => {
// Debounce user input
clearTimeout(event.target._throttle);
event.target._throttle = setTimeout(() => {
// Navigate search-results element on user input
new VV(document.querySelector("search-results")).navigate(`/search?query=${event.target.value}`);
}, DEBOUNCE_TIMEOUT_MS);
});

View file

@ -33,7 +33,7 @@
<option value="null">All</option>
<optgroup label="Categories">
<?php foreach (SearchCategoryEnum::names() as $category): ?>
<?php foreach (SearchCategoryEnum::TABLEs() as $category): ?>
<?php $category = SearchCategoryEnum::fromName($category); ?>
<option value="<?= $category->name ?>" <?= $search::get_category() === $category ? "selected" : "" ?>><?= ucfirst(strtolower($category->name)) ?></option>
<?php endforeach; ?>

View file

@ -37,7 +37,6 @@
//--><!]]>
</script>
<?php // Bootstrapping ?>
<style><?= VV::css("public/assets/css/fonts") ?></style>
<style><?= VV::css("public/assets/css/shell") ?></style>
@ -69,8 +68,7 @@
</div>
</search-results>
<?php // Bootstrapping ?>
<?= VV::init() ?>
<script type="module"><?= VV::js("public/assets/js/shell") ?></script>
<script><?= VV::js("public/assets/js/shell") ?></script>
</body>
</html>

View file

@ -1,13 +1,11 @@
<?php
use VLW\Database\Models\Work\Work;
use const VLW\{TIMELINE_PREVIEW_LIMIT_PARAM, TIMELINE_PREVIEW_LIMIT_COUNT};
require_once VV::root("src/Consts.php");
require_once VV::root("src/Database/Models/Work/Work.php");
?>
<style><?= VV::css("public/assets/css/pages/work") ?></style>
<style><?= VV::css("public/assets/css/pages/work/index") ?></style>
<section class="hero">
<div class="item vegvisir">
<div class="wrapper">
@ -15,7 +13,7 @@
<?= VV::embed("public/assets/media/icons/vegvisir.svg") ?>
<h1>vegvisir</h1>
</div>
<p><?= (new Work("vlw/vegvisir"))->summary() ?></p>
<p><?= Work::from("vlw/vegvisir")->summary ?></p>
<div class="actions">
<a href="https://vegvisir.vlw.se"><button class="inline solid">
<p>read more</p>
@ -30,7 +28,7 @@
<?= VV::embed("public/assets/media/icons/reflect.svg") ?>
<h1>reflect</h1>
</div>
<p><?= (new Work("vlw/reflect"))->summary() ?></p>
<p><?= Work::from("vlw/reflect")->summary ?></p>
<div class="actions">
<a href="https://reflect.vlw.se"><button class="inline solid">
<p>read more</p>
@ -60,7 +58,7 @@
<?= VV::embed("public/assets/media/icons/repo.svg") ?>
</div>
<h3>vlw/php-mysql</h3>
<p><?= (new Work("vlw/php-mysql"))->summary() ?></p>
<p><?= Work::from("vlw/php-mysql")->summary ?></p>
<div class="actions">
<a href="https://codeberg.org/vlw/php-mysql"><button class="inline">
<?= VV::embed("public/assets/media/icons/codeberg.svg") ?>
@ -97,7 +95,7 @@
<a href="/work/archive?href=https://icellate.srv.vlw.se"><img src="/assets/media/img/preview-icellate.avif"></a>
</div>
<h3>Website for iCellate Medical</h3>
<p><?= (new Work("icellate/website"))->summary() ?></p>
<p><?= Work::from("icellate/website")->summary ?></p>
<div class="actions">
<a href="/work/archive?href=https://icellate.srv.vlw.se"><button class="inline">
<?= VV::embed("public/assets/media/icons/star.svg") ?>
@ -111,7 +109,7 @@
<a href="/work/archive?href=https://genemate.srv.vlw.se"><img src="/assets/media/img/preview-genemate.avif"></a>
</div>
<h3>Website for GeneMate by iCellate</h3>
<p><?= (new Work("icellate/genemate"))->summary() ?></p>
<p><?= Work::from("icellate/genemate")->summary ?></p>
<div class="actions">
<a href="/work/archive?href=https://genemate.srv.vlw.se"><button class="inline">
<?= VV::embed("public/assets/media/icons/star.svg") ?>
@ -125,7 +123,7 @@
<a href="/work/deltaco/asyncapp"><img src="/assets/media/img/preview-deltaco.avif"></a>
</div>
<h3>Campaign pages for Deltaco AB</h3>
<p><?= (new Work("deltaco/asyncapp"))->summary() ?></p>
<p><?= Work::from("deltaco/asyncapp")->summary ?></p>
<div class="actions">
<a href="/work/deltaco/asyncapp"><button class="inline">
<p>read more</p>

1
reflect Submodule

@ -0,0 +1 @@
Subproject commit 59c45d52c1845da6b1b06ec5af2e676e327c014d

View file

@ -1,18 +1,15 @@
<?php
namespace VLW\API;
use Reflect\Client as ReflectClient;
class Client extends ReflectClient {
// ISO 8601: YYYY-MM-DD
public const DATE_FORMAT = "Y-m-d";
use Reflect\Path;
use Reflect\Rules\Ruleset;
public function __construct() {
parent::__construct(
$_ENV["client_api"]["base_url"],
$_ENV["client_api"]["api_key"],
$_ENV["client_api"]["verify_peer"]
);
require_once Path::root("vegvisir/src/kernel/Init.php");
require_once Path::root("vegvisir/src/request/VV.php");
class API {
public function __construct(?Ruleset $ruleset = null) {
$ruleset and $ruleset->validate_or_exit();
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace VLW\API;
use vlw\xEnum;
// Enum of all available VLW endpoints grouped by category
enum Endpoints: string {
use xEnum;
case WORK = "/work";
case SEARCH = "/search";
case COFFEE = "/coffee";
case MESSAGES = "/messages";
case WORK_TAGS = "/work/tags";
case WORK_ACTIONS = "/work/actions";
case COFFEE_STATS = "/coffee/stats";
case WORK_TIMELINE = "/work/timeline";
case ABOUT_LANGUAGES = "/about/languages";
}

View file

@ -2,44 +2,17 @@
namespace VLW\Database;
use Reflect\Response;
use vlw\MySQL\MySQL;
class Database {
public const SIZE_UUID = 36;
public const SIZE_TEXT = 65535;
public const SIZE_UINT8 = 2 ** 8 -1;
public const SIZE_UINT16 = 2 ** 16 -1;
public const SIZE_UINT32 = 2 ** 32 -1;
public const SIZE_UINT64 = 2 ** 64 -1;
public const SIZE_VARCHAR = 255;
protected readonly MySQL $db;
class Database extends MySQL {
public const DATE_FORMAT = "Y-m-d H:i:s"; // RFC 3339
public function __construct() {
// Create new MariaDB connection
$this->db = new MySQL(
$_ENV["server_database"]["host"],
$_ENV["server_database"]["user"],
$_ENV["server_database"]["pass"],
$_ENV["server_database"]["db"],
parent::__construct(
$_ENV["mariadb"]["host"],
$_ENV["mariadb"]["user"],
$_ENV["mariadb"]["pass"],
$_ENV["mariadb"]["db"],
);
}
// Return all rows from a table using $_GET paramters as the WHERE clause as a Reflect\Response
public function list(string $table, array $columns, array $order = null): Response {
$resp = $this->db->for($table)->where(array_intersect_key($_GET, array_flip($columns)));
// Optionally order rows by columns
if ($order) {
$resp = $resp->order($order);
}
// Return all matched rows or a 404 with an empty array if there were not results
$resp = $resp->select($columns);
return $resp && $resp->num_rows > 0
? new Response($resp->fetch_all(MYSQLI_ASSOC))
: new Response([], 404);
}
}

View file

@ -1,30 +0,0 @@
<?php
namespace VLW\Database\Models\About;
use \VV;
use VLW\API\Endpoints;
use VLW\Database\Models\Model;
use VLW\Database\Tables\About\LanguagesTable;
require_once VV::root("src/Consts.php");
require_once VV::root("src/API/Endpoints.php");
require_once VV::root("src/Database/Models/Model.php");
require_once VV::root("src/Database/Tables/About/Languages.php");
class Language extends Model {
public function __construct(public readonly string $id) {
parent::__construct(Endpoints::ABOUT_LANGUAGES, [
LanguagesTable::ID->value => $this->id
]);
}
public static function all(array $params = []): array {
return array_map(fn(array $item): Language => new Language($item[LanguagesTable::ID->value]), parent::list(Endpoints::ABOUT_LANGUAGES, $params));
}
public function bytes(): int {
return $this->get(LanguagesTable::BYTES->value);
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace VLW\Database\Models\Languages;
use \VV;
use \vlw\MySQL\Order;
use VLW\Helpers\UUID;
use VLW\Database\Database;
use VLW\Database\Models\Model;
use VLW\Database\Tables\Work\Languages;
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/Languages/Languages.php");
class Language extends Model {
final public static function new(string $name, ?int $bytes = 0): self {
$id = UUID::v4();
if (!parent::create(Languages::TABLE, [
Languages::ID->value => $id,
Languages::NAME->value => $name,
Languages::BYTES->value => $bytes
])) { throw new Exception("Failed to create Language entity"); }
return new Language($id);
}
final public static function from(string $name): ?self {
return array_map(fn(array $language): Language => new Language($language[Languages::ID->value]), new Database()
->from(Languages::TABLE)
->where([Languages::NAME->value => $name])
->limit(1)
->select(Languages::ID->value)
->fetch_all(MYSQLI_ASSOC)
);
}
public function __construct(public readonly string $id) {
parent::__construct(Languages::TABLE, Languages::values(), [
Languages::ID->value => $this->id
]);
}
final public int $name {
get => $this->get(Languages::NAME->value);
set (int $name) => $this->set(Languages::NAME->value, $name);
}
final public int $bytes {
get => $this->get(Languages::BYTES->value);
set (int $bytes) => $this->set(Languages::BYTES->value, $bytes);
}
}

View file

@ -4,40 +4,56 @@
use \VV;
use VLW\API\{Client, Endpoints};
use VLW\Database\Database;
require_once VV::root("src/API/API.php");
require_once VV::root("src/API/Endpoints.php");
require_once VV::root("src/Database/Database.php");
abstract class Model {
abstract public static function all(array $params = []): array;
private array $row;
const DATE_FORMAT = Database::DATE_FORMAT;
abstract public string $id { get; }
protected readonly Database $db;
private bool $_resolved = false;
private array $_row;
public function __construct(
public readonly ?Endpoints $endpoint = null,
private readonly array $params = []
private readonly string $table,
private readonly array $columns,
private readonly array $where
) {
if ($this->endpoint) {
$this->assign(self::first(self::list($endpoint, $params)));
$this->db = new Database();
}
private ?array $row {
get {
// Return existing row data
if ($this->_resolved) { return $this->_row; }
$this->_resolved = true;
return $this->_row = $this->db
->from($this->table)
->where($this->where)
->limit(1)
->select($this->columns)
->fetch_assoc() ?? [];
}
}
public static function first(array $array): array {
return $array && is_array(array_values($array)[0]) ? $array[0] : $array;
}
public static function list(Endpoints $endpoint, array $params = []): array {
$resp = (new Client())->call($endpoint->value)->params($params)->get();
return $resp->ok ? $resp->json() : [];
}
public function assign(array $row): self {
$this->row = $row;
return $this;
protected static function create(string $table, array $values): bool {
return new Database()->from($table)->insert($values);
}
public function get(string $key): mixed {
return $this->row[$key] ?? null;
}
public function set(string $key, mixed $value): bool {
$this->_row[$key] = $value;
return $this->db
->from($this->table)
->where($this->where)
->update([$key => $value]);
}
}

View file

@ -4,48 +4,75 @@
use \VV;
use VLW\API\Endpoints;
use VLW\Helpers\UUID;
use VLW\Database\Models\Model;
use VLW\Database\Models\Work\Work;
use VLW\Database\Tables\Work\ActionsTable;
use VLW\Database\Tables\Work\Actions;
require_once VV::root("src/API/Endpoints.php");
require_once VV::root("src/Helpers/UUID.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");
class Action extends Model {
public function __construct() {
parent::__construct();
final public static function new(Work $work): self {
$id = UUID::v4();
if (!parent::create(Actions::TABLE, [
Actions::ID->value => $id,
Actions::REF_WORK_ID->value => $work->id,
Actions::HREF->value => null,
Actions::CLASSLIST->value => null,
Actions::ICON_PREPEND->value => null,
Actions::ICON_APPEND->value => null
])) { throw new Exception("Failed to create Work Action entity"); }
return new Action($id);
}
public static function all(array $params = []): array {
return array_map(fn(array $item): Action => (new Action())->assign($item), parent::list(Endpoints::WORK_ACTIONS, $params));
final public static function from(Work $work): array {
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])
->select(Actions::ID->value)
->fetch_all(MYSQLI_ASSOC)
);
}
public static function from(Work $work): array {
return self::all([
ActionsTable::REF_WORK_ID->value => $work->id
public function __construct(public readonly string $id) {
parent::__construct(Actions::TABLE, Actions::values(), [
Actions::ID->value => $this->id
]);
}
public function icon_prepended(): ?string {
return $this->get(ActionsTable::ICON_PREPENDED->value);
final public Work $work {
get => $this->get(Actions::REF_WORK_ID->value);
set (Work $work) => $this->set(Actions::REF_WORK_ID->value, $work);
}
public function icon_appended(): ?string {
return $this->get(ActionsTable::ICON_APPENDED->value);
final public int $order_idx {
get => $this->get(TimelineTable::ORDER_IDX->value);
set (int $order_idx) => $this->set(TimelineTable::ORDER_IDX->value, $order_idx);
}
public function display_text(): string {
return $this->get(ActionsTable::DISPLAY_TEXT->value);
final public ?string $href {
get => $this->get(Actions::HREF->value);
set (?string $href) => $this->set(Actions::HREF->value, $href);
}
public function href(): ?string {
return $this->get(ActionsTable::HREF->value);
final public ?string $classlist {
get => $this->get(Actions::CLASSLIST->value);
set (?string $classlist) => $this->set(Actions::CLASSLIST->value, $classlist);
}
public function classes(): array {
return $this->get(ActionsTable::CLASS_LIST->value) ? explode(",", $this->get(ActionsTable::CLASS_LIST->value)) : [];
final public ?string $icon_prepend {
get => $this->get(Actions::ICON_PREPEND->value);
set (?string $icon_prepend) => $this->set(Actions::ICON_PREPEND->value, $icon_prepend);
}
final public ?string $icon_append {
get => $this->get(Actions::ICON_APPEND->value);
set (?string $icon_append) => $this->set(Actions::ICON_APPEND->value, $icon_append);
}
}

View file

@ -3,33 +3,56 @@
namespace VLW\Database\Models\Work;
use \VV;
use \vlw\MySQL\Order;
use VLW\API\Endpoints;
use VLW\Helpers\UUID;
use VLW\Database\Database;
use VLW\Database\Models\Model;
use VLW\Database\Models\Work\Work;
use VLW\Database\Tables\Work\{TagsTable, TagsLabelEnum};
use VLW\Database\Tables\Work\{Tags, TagsLabelEnum};
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/Models/Work/Work.php");
require_once VV::root("src/Database/Tables/Work/Tags.php");
class Tag extends Model {
public function __construct() {
parent::__construct();
final public static function new(Work $work, TagsLabelEnum $label): self {
$id = UUID::v4();
if (!parent::create(TimelineTable::TABLE, [
TimelineTable::ID->value => $id,
TimelineTable::REF_WORK_ID->value => $work->id,
TimelineTable::LABEL->value => $label->name
])) { throw new Exception("Failed to create Work Tag entity"); }
return new Tag($id);
}
public static function all(array $params = []): array {
return array_map(fn(array $item): Tag => (new Tag())->assign($item), parent::list(Endpoints::WORK_TAGS, $params));
final public static function from(Work $work): array {
return array_map(fn(array $tag): Tag => new Tag($tag[Tags::ID->value]), new Database()
->from(Tags::TABLE)
->where([Tags::REF_WORK_ID->value => $work->id])
->order([Tags::LABEL->value => Order::DESC])
->select(Tags::ID->value)
->fetch_all(MYSQLI_ASSOC)
);
}
public static function from(Work $work): array {
return self::all([
TagsTable::REF_WORK_ID->value => $work->id
public function __construct(public readonly string $id) {
parent::__construct(Tags::TABLE, Tags::values(), [
Tags::ID->value => $this->id
]);
}
public function label(): TagsLabelEnum {
return TagsLabelEnum::fromName($this->get(TagsTable::LABEL->value));
final public Work $work {
get => new Work($this->get(Tags::REF_WORK_ID->value));
set (Work $work) => $this->set(Tags::REF_WORK_ID->value, $work);
}
final public TagsLabelEnum $label {
get => TagsLabelEnum::fromName($this->get(Tags::LABEL->value));
set (TagsLabelEnum $pathname) => $this->set(Tags::LABEL->value, $pathname->name);
}
}

View file

@ -4,40 +4,54 @@
use \VV;
use VLW\API\Endpoints;
use VLW\Helpers\UUID;
use VLW\Database\Models\Model;
use VLW\Database\Models\Work\Work;
use VLW\Database\Tables\Work\TimelineTable;
use VLW\Database\Tables\Work\Timeline as TimelineTable;
require_once VV::root("src/API/Endpoints.php");
require_once VV::root("src/Helpers/UUID.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");
class Timeline extends Model {
final public static function new(Work $work): self {
$id = UUID::v4();
if (!parent::create(TimelineTable::TABLE, [
TimelineTable::ID->value => $id,
TimelineTable::REF_WORK_ID->value => $work->id,
TimelineTable::YEAR->value => (int) $work->date_created->format("Y"),
TimelineTable::MONTH->value => (int) $work->date_created->format("n"),
TimelineTable::DAY->value => (int) $work->date_created->format("j"),
])) { throw new Exception("Failed to create Work Timeline entity"); }
return new Timeline($id);
}
public function __construct(public readonly string $id) {
parent::__construct(Endpoints::WORK_TIMELINE, [
TimelineTable::REF_WORK_ID->value => $this->id
parent::__construct(TimelineTable::TABLE, TimelineTable::values(), [
TimelineTable::ID->value => $this->id
]);
}
public static function all(array $params = []): array {
return array_map(fn(array $item): Timeline => new Timeline($item[TimelineTable::REF_WORK_ID->value]), parent::list(Endpoints::WORK_TIMELINE, $params));
final public Work $work {
get => $this->get(WorkTable::REF_WORK_ID->value);
set (Work $work) => $this->set(WorkTable::REF_WORK_ID->value, $work->id);
}
public function work(): Work {
return new Work($this->get(TimelineTable::REF_WORK_ID->value));
final public int $year {
get => $this->get(TimelineTable::YEAR->value);
set (int $year) => $this->set(TimelineTable::YEAR->value, $year);
}
public function year(): int {
return $this->get(TimelineTable::YEAR->value);
final public int $month {
get => $this->get(TimelineTable::MONTH->value);
set (int $month) => $this->set(TimelineTable::MONTH->value, $month);
}
public function month(): int {
return $this->get(TimelineTable::MONTH->value);
}
public function day(): int {
return $this->get(TimelineTable::DAY->value);
final public int $day {
get => $this->get(TimelineTable::DAY->value);
set (int $day) => $this->set(TimelineTable::DAY->value, $day);
}
}

View file

@ -3,48 +3,88 @@
namespace VLW\Database\Models\Work;
use \VV;
use \vlw\MySQL\Order;
use \DateTimeImmutable;
use VLW\API\Endpoints;
use VLW\Helpers\UUID;
use VLW\Database\Database;
use VLW\Database\Models\Model;
use VLW\Database\Tables\Work\WorkTable;
use VLW\Database\Models\Work\{Tag, Action};
use VLW\Database\Tables\Work\Work as WorkTable;
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/Models/Work/Tag.php");
require_once VV::root("src/Database/Tables/Work/Work.php");
require_once VV::root("src/Database/Models/Work/Action.php");
class Work extends Model {
public const DATE_FORMAT = "Y-m-d";
final public static function new(string $namespace): self {
$id = UUID::v4();
if (!parent::create(WorkTable::TABLE, [
WorkTable::ID->value => $id,
WorkTable::NAMESPACE->value => $namespace,
WorkTable::TITLE->value => $pathname,
WorkTable::SUMMARY->value => null,
WorkTable::DATE_CREATED->value => date(parent::DATE_FORMAT)
])) { throw new Exception("Failed to create Work entity"); }
return new Work($id);
}
final public static function all(): array {
return array_map(fn(array $work): Work => new Work($work[WorkTable::ID->value]), new Database()
->from(WorkTable::TABLE)
->order([WorkTable::DATE_CREATED->value => Order::DESC])
->select(WorkTable::ID->value)
->fetch_all(MYSQLI_ASSOC)
);
}
final public static function from(string $namespace): ?self {
$work = new Database()
->from(WorkTable::TABLE)
->where([WorkTable::NAMESPACE->value => $namespace])
->limit(1)
->select(WorkTable::ID->value)
->fetch_assoc();
return $work ? new Work($work[WorkTable::ID->value]) : null;
}
public function __construct(public readonly string $id) {
parent::__construct(Endpoints::WORK, [
parent::__construct(WorkTable::TABLE, WorkTable::values(), [
WorkTable::ID->value => $this->id
]);
}
public static function all(array $params = []): array {
return array_map(fn(array $item): Work => new Work($item[WorkTable::ID->value]), parent::list(Endpoints::WORK, $params));
}
public function title(): ?string {
return $this->get(WorkTable::TITLE->value);
}
public function summary(): string {
return $this->get(WorkTable::SUMMARY->value);
}
public function created(): \DateTimeImmutable {
return new \DateTimeImmutable($this->get(WorkTable::CREATED->value));
}
public function tags(): array {
final public function tags(): array {
return Tag::from($this);
}
public function actions(): array {
final public function actions(): array {
return Action::from($this);
}
final public string $namespace {
get => $this->get(WorkTable::NAMESPACE->value);
set (string $namespace) => $this->set(WorkTable::NAMESPACE->value, $namespace);
}
final public string $title {
get => $this->get(WorkTable::TITLE->value);
set (string $title) => $this->set(WorkTable::TITLE->value, $title);
}
final public ?string $summary {
get => $this->get(WorkTable::SUMMARY->value);
set (?string $summary) => $this->set(WorkTable::SUMMARY->value, $summary);
}
final public DateTimeImmutable $date_created {
get => new DateTimeImmutable($this->get(WorkTable::DATE_CREATED->value));
set (DateTimeImmutable $date_created) => $this->set(WorkTable::DATE_CREATED->value, $date_created->format(parent::DATE_FORMAT));
}
}

View file

@ -8,276 +8,82 @@ SET time_zone = "+00:00";
/*!40101 SET NAMES utf8mb4 */;
CREATE TABLE `about_languages` (
`id` varchar(255) NOT NULL,
`bytes` int(10) UNSIGNED NOT NULL
) ENGINE=MEMORY DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `coffee` (
`id` int(32) NOT NULL
`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 `after_coffee_insert` AFTER INSERT ON `coffee` FOR EACH ROW BEGIN
DECLARE count_recent INT;
DECLARE count_average INT;
-- Clear the stats table --
DELETE FROM coffee_stats;
-- Count the number of rows with timestamp less than 7 days ago
SELECT COUNT(*) INTO count_recent
FROM coffee
WHERE id > UNIX_TIMESTAMP(NOW() - INTERVAL 7 DAY);
-- Calculate the average count of rows for each week
SELECT COUNT(*) / COUNT(DISTINCT YEAR(FROM_UNIXTIME(id)), WEEK(FROM_UNIXTIME(id)))
INTO count_average
FROM coffee;
-- Insert the count into the stats table
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=MEMORY DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `languages` (
`id` char(36) NOT NULL,
`name` varchar(255) NOT NULL,
`bytes` int(10) UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `messages` (
`id` char(36) NOT NULL,
`email` varchar(255) DEFAULT NULL,
`message` text NOT NULL,
`timestamp_created` int(32) NOT NULL
`date_created` datetime NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `search` (
`query` varchar(2048) NOT NULL,
`id` char(10) NOT NULL,
`title` varchar(2048) DEFAULT NULL,
`summary` varchar(2048) NOT NULL,
`category` enum('WORK') DEFAULT NULL,
`href` varchar(2048) DEFAULT NULL
) ENGINE=MEMORY DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `work` (
`id` varchar(255) NOT NULL,
`title` varchar(255) DEFAULT NULL,
`summary` text NOT NULL,
`created` date NOT NULL
`id` char(36) NOT NULL,
`namespace` varchar(255) NOT NULL,
`title` varchar(255) NOT NULL,
`summary` text DEFAULT NULL,
`date_created` datetime NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `work` (`id`, `title`, `summary`, `created`) VALUES
('deltaco/asyncapp', 'Campaign pages for Deltaco', 'From design mock-ups created by the SweDeltaco marketing team, I built various web pages for campagins and special events for the nordic IT-distributor\'s website using a custom content injection framework for SharePoint that would later inspire my other project, Vegvisir.', '2020-10-18'),
('deltaco/distit', 'WordPress modules for DistIT', 'Maintenance of a web server for DistIT\'s WordPress website. DistIT is the parent company of SweDeltaco where I was employed for a few years. In addition to server maintenance, I also wrote a few custom WordPress modules for the site, and helped update DistIT\'s custom WordPress theme with new content when required.', '2021-01-21'),
('deltaco/e-charge', 'Product guide for Deltaco E-Charge', 'Front- and back-end for a product configurator from a design mock-up created by one of SweDeltaco\'s graphics designers. The configurator was for Deltaco\'s \"E-charge\"-line of EV-charger products.', '2021-04-06'),
('deltaco/office', 'Product guide for Deltaco Office', 'Product configurator of my own design for Deltaco\'s \"Deltaco Office\"-line of products. The configurator is open source and was implemented by various big-name brands of resellers across the nordics.', '2020-09-04'),
('deltaco/pdf-generator', 'PDF datasheet generator for Deltaco', 'Custom PDF generator for SharePoint 2013', '2020-09-04'),
('deltaco/reseller-form', 'Customer registration form for Deltaco', 'Custom web form which integrated with existing back-end infrastructure to handle new authorized resellers of Deltaco\'s assortment.', '2020-08-04'),
('icellate/genemate', 'Website for GeneMate by iCellate Medical', 'Together with copy written by the marketing team at iCellate, and a new brand new appearance for the company, I helped design a new website and underlying systems for their GeneMate product.', '2022-11-07'),
('icellate/website', 'Website for iCellate Medical', 'Together with the iCellate team, I created a new front-end for the biopharma startup using my Vegvisir framework as the foundation.', '2023-04-19'),
('itg/lan', 'Reservation website for ITG-Sundbyberg', 'Redesign of IT-Gymnasiet Sundbyberg\'s seat reservation system, tournament registration, and information website for their yearly LAN events.', '2014-09-02'),
('itg/upload', 'Web project upload for ITG-Sundbyberg', 'Special school assignment for my Web programming course at IT-Gymnasiet Sundbyberg', '2014-06-11'),
('vlw/bbcb', 'Big Black Coffee Button', 'A very simple PWA for updating the \"coffe tally\" on my about page from anywhere in the world whenever I have a cup of coffee!', '2025-03-13'),
('vlw/camera-obscura', 'cameraobscura.gr', 'Portable front-end website for Camera Obscura GR', '2018-04-25'),
('vlw/collage', 'vlw/collage', 'Create an image where each \"pixel\" is a smaller image of similar color to the original image.', '2021-03-21'),
('vlw/curl', 'cURL wrapper Bash script', 'Public domain shell script optimized to be run in a Visual Studio Code-like interface that wraps cURL on any system with Bash installed. I created it to make manual requests for Reflect endpoints with API Bearer token keys.', '2025-02-09'),
('vlw/dediprison', 'DediPrison', 'Public Minecraft server project together with a friend that had around 20-30 active monthly players.', '2015-10-13'),
('vlw/disneyplus-pip', 'vlw/disneyplus-pip', 'Enable (or rather disable Disney\'s block of) picture-in-picture on disneyplus.com for Chrome.', '2021-01-31'),
('vlw/edkb', 'vlw/edkb', 'Printable keyboard overlay for some controls in Elite Dangerous.', '2021-03-18'),
('vlw/elevent', 'vlw/elevent', 'A small npm module that is intended to add more control over event listeners on HTMLElements with JavaScript. Kind of a superset of addEventListener.', '2024-11-11'),
('vlw/eyeart', 'eyeart.me', 'Website designed by me for the Greek/Swedish photographer, eyeart. The website features albums, a blog, and news pages.', '2014-03-02'),
('vlw/href', 'API-managed permalink redirector/URL shortener', 'This is a simple API-managed permalink generator/URL shortener that I created to hotlink resources for my projects. Permalink destinations can be altered if the target resource needs to be moved. Permalinks can also replace other permalinks with native inheritance at the database-level', '2025-02-09'),
('vlw/ion-musik', 'Website for ION Musik', 'Portable front-end website for Greek musican, ION Musik.', '2015-06-11'),
('vlw/labylib', 'LabyLib', 'Library for controlling LabyMod cosmetics programmatically in Python.', '2020-11-11'),
('vlw/labylib-animated-cape', 'vlw/labylib-animated-cape', 'Minecraft cosmetics scripts for my labylib library that cycles between a set of Labymod capes, creating a (slow) animation.', '2020-11-15'),
('vlw/labylib-chattycape', 'vlw/labylib-chattycape', 'Minecraft cosmetics update script for my labylib library that drew a picture of the last person who sent something in chat.', '2020-11-15'),
('vlw/misskey-microblogger', 'vlw/misskey-microblogger', 'Bot program for Misskey (and compatible forks) that simulates a whole community of independent microbloggers with posts, reactions, and replies. Users have unique personalities, friend groups, partners, and even enemies to whom they will act and respond differently to, sometimes not at all.', '2024-11-09'),
('vlw/monkeydo', 'MonkeyDo', 'A multi-threaded keyframe animation library for JavaScript.', '2021-10-08'),
('vlw/php-age', 'vlw/php-age', 'Asymmetric encryption and decryption of files from PHP with this wrapper for the age command line tool.', '2023-08-22'),
('vlw/php-functionflags', 'vlw/php-functionflags', '', '2023-03-16'),
('vlw/php-globalsnapshot', 'vlw/php-globalsnapshot', '\"Proxy\" PHP superglobals by taking snapshots of current values. The snapshotted state can then be restored at any point.', '2024-04-18'),
('vlw/php-mime-types', 'vlw/php-mime-types', 'Library for resolving a RFC 4288-compatible MIME-type list. After loading a list, files on disk can be queried for types and extensions.', '2024-10-27'),
('vlw/php-mysql', 'vlw/php-mysql', 'Yet another abstraction library for the php-mysql extension. For this library, I was willing to sacrifice most of MySQL\'s flexibility that comes with string interpolation in favor of method chaining that adheres to an SQL-like syntax. For simple DML operations I think it\'s pretty intuitive.', '2023-04-08'),
('vlw/php-sqlite', 'vlw/php-sqlite', 'Abstraction library for common DML queries on an SQLite database with php-sqlite3.', '2023-04-18'),
('vlw/php-xenum', 'vlw/php-xenum', 'Adds a variety of missing quality-of-life methods to PHP 8.0 Enums.', '2023-06-12'),
('vlw/pysheeter', 'Pysheeter', 'Sprite sheet generator for PNGs in Python.', '2020-11-20'),
('vlw/reflect', 'Reflect', 'A weird framework for building REST APIs in PHP with focus on native internal request routing and proxying.', '2022-11-18'),
('vlw/stadia-avatar', 'vlw/stadia-avatar', '', '2021-02-03'),
('vlw/still-alive', 'vlw/still-alive', '', '2021-10-12'),
('vlw/vegvisir', 'Vegvisir', 'Web navigation framework for PHP websites that does on the fly MPA-to-SPA routing between pages on the [open] web seas.', '2022-11-26'),
('vlw/vlw.se', 'vlw.se', 'Can I put my own website here, is that cheating? Maybe, but I think this site counts as the most important thing I\'ve personally created. I\'ve only used my own libraries and frameworks to create this website, so it kind of works as a live demonstration of many of my web projects bundled together.', '2023-06-13');
CREATE TABLE `work_actions` (
`ref_work_id` varchar(255) NOT NULL,
`icon_prepended` varchar(255) DEFAULT NULL,
`icon_appended` varchar(255) DEFAULT NULL,
`order_idx` tinyint(1) UNSIGNED NOT NULL DEFAULT 0,
`display_text` varchar(255) NOT NULL,
`id` char(36) NOT NULL,
`ref_work_id` char(36) NOT NULL,
`order_idx` tinyint(3) UNSIGNED NOT NULL DEFAULT 0,
`href` varchar(255) DEFAULT NULL,
`class_list` varchar(255) DEFAULT NULL
`classlist` varchar(255) DEFAULT NULL,
`icon_prepend` varchar(255) DEFAULT NULL,
`icon_append` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `work_actions` (`ref_work_id`, `icon_prepended`, `icon_appended`, `order_idx`, `display_text`, `href`, `class_list`) VALUES
('vlw/collage', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/collage', NULL),
('vlw/disneyplus-pip', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/disneyplus-pip', NULL),
('vlw/edkb', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/edkb', NULL),
('vlw/labylib', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/labylib', NULL),
('vlw/monkeydo', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/monkeydo', NULL),
('vlw/php-age', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/php-age', NULL),
('vlw/php-mysql', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/php-mysql', NULL),
('vlw/php-xenum', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/php-xenum', NULL),
('vlw/pysheeter', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/pysheeter', NULL),
('vlw/vlw.se', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/vlw.se', NULL),
('vlw/elevent', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/elevent', NULL),
('vlw/misskey-microblogger', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/misskey-microblogger', ''),
('vlw/php-mime-types', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/php-mime-types', NULL),
('vlw/php-globalsnapshot', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/php-globalsnapshot', NULL),
('vlw/php-sqlite', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/php-sqlite', NULL),
('vlw/php-functionflags', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/functionflags', NULL),
('vlw/still-alive', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/still-alive', NULL),
('vlw/still-alive', 'star.svg', NULL, 0, 'open demo', 'https://victorwesterlund.github.io/still-alive/', NULL),
('vlw/bbcb', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/big-black-coffee-button', NULL),
('vlw/href', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/href', NULL),
('vlw/curl', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/curl', NULL);
CREATE TABLE `work_tags` (
`ref_work_id` varchar(255) NOT NULL,
`label` enum('VLW','RELEASE','WEBSITE','REPO') NOT NULL
`id` char(36) NOT NULL,
`ref_work_id` char(36) NOT NULL,
`label` enum('VLW','WEBSITE','REPO') NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `work_tags` (`ref_work_id`, `label`) VALUES
('vlw/eyeart', 'WEBSITE'),
('itg/upload', 'WEBSITE'),
('itg/lan', 'WEBSITE'),
('vlw/dediprison', 'VLW'),
('vlw/dediprison', 'WEBSITE'),
('vlw/ion-musik', 'WEBSITE'),
('vlw/camera-obscura', 'WEBSITE'),
('deltaco/asyncapp', 'REPO'),
('deltaco/reseller-form', 'WEBSITE'),
('deltaco/pdf-generator', 'REPO'),
('deltaco/office', 'WEBSITE'),
('deltaco/distit', 'WEBSITE'),
('deltaco/e-charge', 'WEBSITE'),
('vlw/labylib', 'VLW'),
('vlw/labylib', 'REPO'),
('vlw/edkb', 'VLW'),
('vlw/collage', 'VLW'),
('vlw/collage', 'REPO'),
('vlw/monkeydo', 'VLW'),
('vlw/monkeydo', 'REPO'),
('vlw/disneyplus-pip', 'VLW'),
('vlw/disneyplus-pip', 'REPO'),
('vlw/php-mysql', 'VLW'),
('vlw/php-mysql', 'REPO'),
('vlw/pysheeter', 'VLW'),
('vlw/pysheeter', 'REPO'),
('vlw/php-age', 'VLW'),
('vlw/php-age', 'REPO'),
('vlw/php-xenum', 'VLW'),
('vlw/php-xenum', 'REPO'),
('vlw/reflect', 'VLW'),
('vlw/reflect', 'REPO'),
('vlw/vegvisir', 'VLW'),
('vlw/vegvisir', 'REPO'),
('icellate/website', 'WEBSITE'),
('icellate/genemate', 'WEBSITE'),
('vlw/elevent', 'VLW'),
('vlw/elevent', 'REPO'),
('vlw/misskey-microblogger', 'VLW'),
('vlw/misskey-microblogger', 'REPO'),
('vlw/php-mime-types', 'VLW'),
('vlw/php-mime-types', 'REPO'),
('vlw/php-globalsnapshot', 'VLW'),
('vlw/php-globalsnapshot', 'REPO'),
('vlw/php-sqlite', 'VLW'),
('vlw/php-sqlite', 'REPO'),
('vlw/php-functionflags', 'VLW'),
('vlw/php-functionflags', 'REPO'),
('vlw/still-alive', 'VLW'),
('vlw/still-alive', 'WEBSITE'),
('vlw/stadia-avatar', 'VLW'),
('vlw/stadia-avatar', 'REPO'),
('vlw/labylib-chattycape', 'VLW'),
('vlw/labylib-chattycape', 'REPO'),
('vlw/labylib-animated-cape', 'VLW'),
('vlw/labylib-animated-cape', 'REPO'),
('vlw/bbcb', 'VLW'),
('vlw/bbcb', 'WEBSITE'),
('vlw/href', 'VLW'),
('vlw/curl', 'VLW');
CREATE TABLE `work_timeline` (
`ref_work_id` varchar(255) NOT NULL,
`id` char(36) NOT NULL,
`ref_work_id` char(36) NOT NULL,
`year` smallint(5) UNSIGNED NOT NULL,
`month` tinyint(3) UNSIGNED NOT NULL,
`day` tinyint(3) UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `work_timeline` (`ref_work_id`, `year`, `month`, `day`) VALUES
('deltaco/asyncapp', 2020, 10, 18),
('deltaco/distit', 2021, 1, 21),
('deltaco/e-charge', 2021, 4, 6),
('deltaco/office', 2020, 9, 4),
('deltaco/pdf-generator', 2020, 9, 4),
('deltaco/reseller-form', 2020, 8, 4),
('icellate/genemate', 2022, 11, 7),
('icellate/website', 2023, 4, 19),
('itg/lan', 2014, 9, 2),
('itg/upload', 2014, 6, 11),
('vlw/bbcb', 2025, 3, 13),
('vlw/camera-obscura', 2018, 4, 25),
('vlw/collage', 2021, 3, 21),
('vlw/curl', 2025, 2, 9),
('vlw/dediprison', 2015, 10, 13),
('vlw/disneyplus-pip', 2021, 1, 31),
('vlw/edkb', 2021, 3, 18),
('vlw/elevent', 2024, 11, 11),
('vlw/eyeart', 2014, 3, 2),
('vlw/href', 2025, 2, 9),
('vlw/ion-musik', 2015, 6, 11),
('vlw/labylib', 2020, 11, 11),
('vlw/labylib-animated-cape', 2020, 11, 15),
('vlw/labylib-chattycape', 2020, 11, 15),
('vlw/misskey-microblogger', 2024, 11, 9),
('vlw/monkeydo', 2021, 10, 8),
('vlw/php-age', 2023, 8, 22),
('vlw/php-functionflags', 2023, 3, 16),
('vlw/php-globalsnapshot', 2024, 4, 18),
('vlw/php-mime-types', 2024, 10, 27),
('vlw/php-mysql', 2023, 4, 8),
('vlw/php-sqlite', 2023, 4, 18),
('vlw/php-xenum', 2023, 6, 12),
('vlw/pysheeter', 2020, 11, 20),
('vlw/reflect', 2022, 11, 18),
('vlw/stadia-avatar', 2021, 2, 3),
('vlw/still-alive', 2021, 10, 12),
('vlw/vegvisir', 2022, 11, 26),
('vlw/vlw.se', 2023, 6, 13);
ALTER TABLE `about_languages`
ADD PRIMARY KEY (`id`);
ALTER TABLE `coffee`
ADD PRIMARY KEY (`id`);
ALTER TABLE `search`
ALTER TABLE `languages`
ADD PRIMARY KEY (`id`),
ADD KEY `keywords` (`query`(768));
ADD UNIQUE KEY `name` (`name`);
ALTER TABLE `work`
ALTER TABLE `messages`
ADD PRIMARY KEY (`id`);
ALTER TABLE `work`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `pathname` (`namespace`);
ALTER TABLE `work_actions`
ADD PRIMARY KEY (`id`),
ADD KEY `ref_work_id` (`ref_work_id`);
ALTER TABLE `work_tags`
ADD KEY `anchor` (`ref_work_id`);
ADD PRIMARY KEY (`id`),
ADD KEY `ref_work_id` (`ref_work_id`);
ALTER TABLE `work_timeline`
ADD PRIMARY KEY (`ref_work_id`);
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `ref_work_id` (`ref_work_id`);
ALTER TABLE `work_actions`

View file

@ -1,98 +0,0 @@
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;
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
('about/languages', 1),
('coffee', 1),
('coffee/stats', 1),
('messages', 1),
('notes', 1),
('playground/coffee', 1),
('search', 1),
('update', 1),
('work', 1),
('work/actions', 1),
('work/tags', 1),
('work/timeline', 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

@ -4,10 +4,10 @@
use vlw\xEnum;
enum CoffeeTable: string {
enum Coffee: string {
use xEnum;
const NAME = "coffee";
const TABLE = "coffee";
case ID = "id";
}

View file

@ -4,10 +4,10 @@
use vlw\xEnum;
enum StatsTable: string {
enum Stats: string {
use xEnum;
const NAME = "coffee_stats";
const TABLE = "coffee_stats";
case COUNT_WEEK = "count_week";
case COUNT_WEEK_AVERAGE = "count_week_average";

View file

@ -4,11 +4,12 @@
use vlw\xEnum;
enum LanguagesTable: string {
enum Languages: string {
use xEnum;
const NAME = "about_languages";
const TABLE = "languages";
case ID = "id";
case NAME = "name";
case BYTES = "bytes";
}

View file

@ -2,8 +2,8 @@
namespace VLW\Database\Tables\Messages;
enum MessagesTable: string {
const NAME = "messages";
enum Messages: string {
const TABLE = "messages";
case EMAIL = "email";
case MESSAGE = "message";

View file

@ -4,16 +4,16 @@
use vlw\xEnum;
enum SearchCategoryEnum {
enum SearchCategory {
use xEnum;
case WORK;
}
enum SearchTable: string {
enum Search: string {
use xEnum;
const NAME = "search";
const TABLE = "search";
case QUERY = "query";
case ID = "id";

View file

@ -3,17 +3,17 @@
namespace VLW\Database\Tables\Work;
use vlw\xEnum;
enum ActionsTable: string {
enum Actions: string {
use xEnum;
const NAME = "work_actions";
case REF_WORK_ID = "ref_work_id";
case ICON_PREPENDED = "icon_prepended";
case ICON_APPENDED = "icon_appended";
case ORDER_IDX = "order_idx";
case DISPLAY_TEXT = "display_text";
case HREF = "href";
case CLASS_LIST = "class_list";
const TABLE = "work_actions";
case ID = "id";
case REF_WORK_ID = "ref_work_id";
case ORDER_IDX = "order_idx";
case HREF = "href";
case CLASSLIST = "classlist";
case ICON_PREPEND = "icon_prepend";
case ICON_APPEND = "icon_append";
}

View file

@ -1,10 +0,0 @@
<?php
namespace VLW\Database\Tables\Work;
enum MediaTable: string {
const NAME = "work_media";
case ANCHOR = "anchor";
case MEDIA = "media";
}

View file

@ -1,15 +0,0 @@
<?php
namespace VLW\Database\Tables\Work;
enum NamespacesTable: string {
const NAME = "work_actions";
case REF_WORK_ID = "ref_work_id";
case ICON_PREFIX = "icon_prefix";
case ICON_SUFFIX = "icon_suffix";
case ORDER_IDX = "order_idx";
case DISPLAY_TEXT = "display_text";
case HREF = "href";
case CLASS_LIST = "class_list";
}

View file

@ -1,11 +0,0 @@
<?php
namespace VLW\Database\Tables\Work;
enum PermalinksTable: string {
const NAME = "work_permalinks";
case SLUG = "slug";
case ANCHOR = "anchor";
case DATE_TIMESTAMP_CREATED = "date_timestamp_created";
}

View file

@ -13,11 +13,12 @@
case REPO;
}
enum TagsTable: string {
enum Tags: string {
use xEnum;
const NAME = "work_tags";
const TABLE = "work_tags";
case ID = "id";
case REF_WORK_ID = "ref_work_id";
case LABEL = "label";
}

View file

@ -4,11 +4,12 @@
use vlw\xEnum;
enum TimelineTable: string {
enum Timeline: string {
use xEnum;
const NAME = "work_timeline";
const TABLE = "work_timeline";
case ID = "id";
case REF_WORK_ID = "ref_work_id";
case YEAR = "year";
case MONTH = "month";

View file

@ -4,13 +4,14 @@
use vlw\xEnum;
enum WorkTable: string {
enum Work: string {
use xEnum;
const NAME = "work";
const TABLE = "work";
case ID = "id";
case TITLE = "title";
case SUMMARY = "summary";
case CREATED = "created";
case ID = "id";
case NAMESPACE = "namespace";
case TITLE = "title";
case SUMMARY = "summary";
case DATE_CREATED = "date_created";
}

View file

@ -1,29 +1,13 @@
<?php
use Reflect\Rules\Ruleset;
use Reflect\{Response, Path, Call};
namespace VLW\Helpers;
use VLW\API\Endpoints;
use VLW\Database\Database;
use VLW\Database\Tables\About\LanguagesTable;
use const VLW\{FORGEJO_ENDPOINT_USER, FORGEJO_ENDPOINT_SEARCH};
use VLW\Database\Models\Languages\Language;
require_once Path::root("src/Consts.php");
require_once Path::root("src/API/Endpoints.php");
require_once Path::root("src/Database/Tables/About/Languages.php");
require_once VV::root("src/Database/Models/Languages/Language.php");
class POST_AboutLanguages extends Database {
protected readonly Ruleset $ruleset;
// Tally of all languages used in all configured repositories
private array $languages = [];
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->validate_or_exit();
parent::__construct();
}
class Forgejo {
private readonly array $languages;
// Fetch JSON from URL
private static function fetch_json(string $url): array {
@ -36,6 +20,10 @@
return self::fetch_json($url);
}
public function __construct() {
$this->languages = [];
}
// Write $this->languages to a JSON file
private function cache_languages(): void {
// Delete existing cache
@ -90,13 +78,4 @@
}
}
public function main(): Response {
$this->add_repositories_from_config_profiles();
// Sort langauges bytes tally by largest in descending order
arsort($this->languages);
$this->cache_languages();
return new Response($this->languages);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace VLW\Helpers;
use \VV;
use VLW\Database\Database;
use VLW\Database\Models\Work\{Work, Timeline};
use VLW\Database\Tables\Work\Timeline as TimelineTable;
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/Work/Timeline.php");
require_once VV::root("src/Database/Tables/Work/Timeline.php");
class GenerateTimeline {
private readonly Database $db;
public function __construct() {
$this->db = new Database();
}
public function generate(): bool {
$this->truncate();
foreach (Work::all() as $work) {
Timeline::new($work);
}
}
private function truncate(): bool {
return $this->db->from(TimelineTable::TABLE)->delete();
}
}

23
src/Helpers/UUID.php Normal file
View file

@ -0,0 +1,23 @@
<?php
namespace VLW\Helpers;
class UUID {
public static function nil(): string {
return "00000000-0000-0000-0000-000000000000";
}
public static function max(): string {
return "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF";
}
public static function v4(): string {
return sprintf("%04x%04x-%04x-%04x-%04x-%04x%04x%04x",
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
}

1
vegvisir Submodule

@ -0,0 +1 @@
Subproject commit 1549af5be7723979b6acd5a0eda1e1ac1a70e672