mirror of
https://codeberg.org/vlw/vlw.se.git
synced 2025-09-13 21:13:40 +02:00
Compare commits
11 commits
Author | SHA1 | Date | |
---|---|---|---|
e8a81b789b | |||
5e9317def5 | |||
8209ea5ecc | |||
85e8e00091 | |||
d4d73e9278 | |||
fafa8c5852 | |||
03492df615 | |||
37f2ac00c3 | |||
d5cc7fa82c | |||
17bd93dca9 | |||
6b5eee505a |
81 changed files with 1531 additions and 1710 deletions
|
@ -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]
|
||||
base_url = ""
|
||||
scan_profiles = ""
|
||||
[config_time_available]
|
||||
time_zone = "Europe/Stockholm"
|
||||
available_to_hour = 0;
|
||||
reply_average_hours = 0;
|
||||
available_from_hour = 0;
|
||||
|
||||
[service_forgejo]
|
||||
url = ""
|
||||
profiles = ""
|
22
.gitignore
vendored
22
.gitignore
vendored
|
@ -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
6
.gitmodules
vendored
Normal 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
|
70
README.md
70
README.md
|
@ -1,67 +1,27 @@
|
|||
# vlw.se
|
||||
This is the source code behind [vlw.se](https://vlw.se) which is my personal website that I have written and designed from the ground up. The website is built on top of my own [web framework](https://vegvisir.vlw.se) and its API is also built on top of my own [API framework](https://reflect.vlw.se).
|
||||
This is the source code behind [vlw.se](https://vlw.se) which is my personal website that I have written and designed from the ground up. The website is built on top of my own [Vegvisir web framework](https://vegvisir.vlw.se) and its optional REST API is built on top of my [Reflect API framework](https://reflect.vlw.se).
|
||||
|
||||
# Installation
|
||||
Here's how you get my website up and running on your own machine. Note, I have only tested this on Linux and the install script we will run later is written in bash.
|
||||
Here's how you get my website up and running on your own machine. Note, I have only tested this on Linux and the install script we will run requires Bash with `coreutils` installed.
|
||||
|
||||
**Make sure you have both of these package managers installed before proceeding:**
|
||||
- [Composer](https://getcomposer.org/)
|
||||
- [NPM](https://www.npmjs.com/)
|
||||
## Prerequisites
|
||||
- A web server
|
||||
- A MariaDB/MySQL server
|
||||
- PHP 8.4 or newer with the following extensions enabled:
|
||||
- - `php8.4-mysql`
|
||||
- - `php8.4-mbstring`
|
||||
- The composer package manager
|
||||
- Bash with `coreutils` installed (for the install script)
|
||||
|
||||
## 1. Clone this repo
|
||||
Clone/download this repo to your machine. Preferably to a non-public directory - the frameworks will handle that.
|
||||
|
||||
Clone this repository with its submodules. Preferably to a non-public directory - the frameworks will handle that.
|
||||
```
|
||||
git clone https://codeberg.org/vlw/vlw.se --depth 1
|
||||
git clone https://codeberg.org/vlw/vlw.se --recurse-submodules --depth 1
|
||||
```
|
||||
|
||||
## 2. Install [Vegvisir](https://vegvisir.vlw.se) and [Reflect](https://reflect.vlw.se)
|
||||
Follow the installation instructions for my web, and API framework. This site uses the default configuration for both frameworks so the only thing you need to do after you've installed both is to point the `root_path` and `endpoints` directory respectively to the directory where you cloned this repo.
|
||||
|
||||
- [Vegvisir installation](https://vegvisir.vlw.se)
|
||||
- [Reflect installation](https://reflect.vlw.se)
|
||||
|
||||
*Example:*
|
||||
```sh
|
||||
# Vegvisir
|
||||
root_path = "/var/www/vlw.se"
|
||||
# Reflect
|
||||
endpoints = "/var/www/vlw.se"
|
||||
## 2. Run the install script
|
||||
Run the `install.sh` script from the root directory of this repository.
|
||||
```
|
||||
|
||||
## 3. Run the install script
|
||||
Run the `install.sh` script from the root of the repo directory. [Make sure you have the required package managers installed](#installation).
|
||||
|
||||
**Example:**
|
||||
```sh
|
||||
# vlw@example:$
|
||||
cd /var/www/vlw.se
|
||||
# vlw@example:/var/www/vlw.se$
|
||||
./install.sh
|
||||
```
|
||||
|
||||
## 4. Import the database templates
|
||||
There's are two SQL files in [`/src/Database/Seeds/`](https://codeberg.org/vlw/vlw.se/src/branch/master/src/Database/Seeds) that you can use to initialize the two databases required for this website.
|
||||
|
||||
- `vlw` - This database has the website data and should be added to the `db` variable under `server_database` in `/.env.ini`
|
||||
- `vlw_reflect` - This is the Reflect database that has all the endpoints pre-configured. You'll have to add your own ACL rules.
|
||||
|
||||
## 5. Set environment variables
|
||||
Make a copy of the `.env.example.ini` file called `.env.ini` from the root directory of the repo. There are a few parameters you can change here but the required ones are the following:
|
||||
|
||||
```ini
|
||||
[client_api]
|
||||
base_url = ""
|
||||
api_key = ""
|
||||
|
||||
[server_database]
|
||||
host = ""
|
||||
user = ""
|
||||
pass = ""
|
||||
db = ""
|
||||
```
|
||||
|
||||
Please refer to the comments in the ini file for more information about each field.
|
||||
|
||||
## Done!
|
||||
That should be it. Navigate to your configured Vegvisir public host!
|
||||
This script will install and configure Vegvisir, Reflect, and the website through a few propmpted steps.
|
29
api/coffee/DELETE.php
Normal file
29
api/coffee/DELETE.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
use Reflect\{Response, Path};
|
||||
use Reflect\Rules\{Ruleset, Rules, Type};
|
||||
|
||||
use VLW\API\API;
|
||||
use VLW\Helpers\UUID;
|
||||
use VLW\Database\Models\Coffee\Coffee;
|
||||
use VLW\Database\Tables\Coffee\Coffee as CoffeeTable;
|
||||
|
||||
require_once Path::root("src/API/API.php");
|
||||
require_once Path::root("src/Database/Models/Coffee/Coffee.php");
|
||||
require_once Path::root("src/Database/Tables/Coffee/Coffee.php");
|
||||
|
||||
final class DELETE_Coffee extends API {
|
||||
public function __construct() {
|
||||
parent::__construct(new Ruleset()->GET([
|
||||
new Rules(CoffeeTable::ID->value)
|
||||
->required()
|
||||
->type(Type::STRING)
|
||||
->min(UUID::LENGTH)
|
||||
->max(UUID::LENGTH)
|
||||
]));
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
return new Response(new Coffee($_GET[CoffeeTable::ID->value])->delete());
|
||||
}
|
||||
}
|
19
api/coffee/GET.php
Normal file
19
api/coffee/GET.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
use Reflect\{Response, Path};
|
||||
|
||||
use VLW\API\API;
|
||||
use VLW\Database\Models\Coffee\Coffee;
|
||||
|
||||
require_once Path::root("src/API/API.php");
|
||||
require_once Path::root("src/Database/Models/Coffee/Coffee.php");
|
||||
|
||||
final class GET_Coffee extends API {
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
return new Response(Coffee::all());
|
||||
}
|
||||
}
|
41
api/coffee/POST.php
Normal file
41
api/coffee/POST.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
use Reflect\{Response, Path};
|
||||
use Reflect\Rules\{Ruleset, Rules, Type};
|
||||
|
||||
use VLW\API\API;
|
||||
use VLW\Database\Models\Coffee\Coffee;
|
||||
use VLW\Database\Tables\Coffee\Coffee as CoffeeTable;
|
||||
|
||||
require_once Path::root("src/API/API.php");
|
||||
require_once Path::root("src/Database/Models/Coffee/Coffee.php");
|
||||
require_once Path::root("src/Database/Tables/Coffee/Coffee.php");
|
||||
|
||||
final class POST_Coffee extends API {
|
||||
public function __construct() {
|
||||
parent::__construct(new Ruleset(strict: true)->POST([
|
||||
new Rules(CoffeeTable::DATE_CREATED->value)
|
||||
->type(Type::STRING)
|
||||
->type(Type::NUMBER)
|
||||
->default(null)
|
||||
]));
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
$datetime = new DateTimeImmutable();
|
||||
|
||||
// Parse DateTime from POST string
|
||||
if ($_POST[CoffeeTable::DATE_CREATED->value]) {
|
||||
try {
|
||||
// Create DateTimeImmutable from Unix timestamp or datetime string
|
||||
$datetime = gettype($_POST[CoffeeTable::DATE_CREATED->value]) === "integer"
|
||||
? DateTimeImmutable::createFromTimestamp($_POST[CoffeeTable::DATE_CREATED->value])
|
||||
: new DateTimeImmutable($_POST[CoffeeTable::DATE_CREATED->value]);
|
||||
} catch (DateMalformedStringException $error) {
|
||||
return new Response($error->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(Coffee::new($datetime));
|
||||
}
|
||||
}
|
19
api/languages/GET.php
Normal file
19
api/languages/GET.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
use Reflect\{Response, Path};
|
||||
|
||||
use VLW\API\API;
|
||||
use VLW\Database\Models\Languages\Language;
|
||||
|
||||
require_once Path::root("src/API/API.php");
|
||||
require_once Path::root("src/Database/Models/Languages/Language.php");
|
||||
|
||||
final class GET_Languages extends API {
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
return new Response(Language::all());
|
||||
}
|
||||
}
|
71
api/update/GET.php
Normal file
71
api/update/GET.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
use \vlw\xEnum;
|
||||
use Reflect\{Response, Path};
|
||||
use Reflect\Rules\{Ruleset, Rules, Type};
|
||||
|
||||
use VLW\API\API;
|
||||
use VLW\Helpers\{
|
||||
Forgejo,
|
||||
GenerateSearch,
|
||||
GenerateTimeline
|
||||
};
|
||||
|
||||
require_once Path::root("src/API/API.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");
|
||||
|
||||
enum ServiceEnum: string {
|
||||
use xEnum;
|
||||
|
||||
case ALL = "all";
|
||||
case SEARCH = "search";
|
||||
case FORGEJO = "forgejo";
|
||||
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 main(): Response {
|
||||
switch ($_GET[self::KEY_SERVICE]) {
|
||||
case ServiceEnum::FORGEJO->value:
|
||||
return new Response($this->update_forgejo());
|
||||
|
||||
case ServiceEnum::SEARCH->value:
|
||||
return new Response($this->update_search());
|
||||
|
||||
case ServiceEnum::TIMELINE->value:
|
||||
return new Response($this->update_timeline());
|
||||
|
||||
case ServiceEnum::ALL->value:
|
||||
default:
|
||||
return new Response(
|
||||
$this->update_timeline() &&
|
||||
$this->update_search() &&
|
||||
$this->update_forgejo()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function update_timeline(): bool {
|
||||
return new GenerateTimeline()->generate();
|
||||
}
|
||||
|
||||
private function update_search(): bool {
|
||||
return new GenerateSearch()->generate();
|
||||
}
|
||||
|
||||
private function update_forgejo(): bool {
|
||||
return new Forgejo()->update();
|
||||
}
|
||||
}
|
28
api/work/GET.php
Normal file
28
api/work/GET.php
Normal 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()));
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"require": {
|
||||
"reflect/client": "dev-master",
|
||||
"vlw/mysql": "dev-master",
|
||||
"vlw/xenum": "dev-master"
|
||||
},
|
||||
|
|
39
composer.lock
generated
39
composer.lock
generated
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,102 +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;
|
||||
use const VLW\{FORGEJO_ENDPOINT_USER, FORGEJO_ENDPOINT_SEARCH};
|
||||
|
||||
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");
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// Fetch JSON from URL
|
||||
private static function fetch_json(string $url): array {
|
||||
return json_decode(file_get_contents($url), true);
|
||||
}
|
||||
|
||||
// Fetch JSON from a Forgejo endpoint
|
||||
private static function fetch_endpoint(string $endpoint): array {
|
||||
$url = $_ENV["server_forgejo"]["base_url"] . $endpoint;
|
||||
return self::fetch_json($url);
|
||||
}
|
||||
|
||||
// Write $this->languages to a JSON file
|
||||
private function cache_languages(): void {
|
||||
// Delete existing cache
|
||||
(new Call(Endpoints::ABOUT_LANGUAGES->value))->delete();
|
||||
|
||||
$this->db->for(LanguagesTable::NAME);
|
||||
|
||||
foreach ($this->languages as $language => $bytes) {
|
||||
$this->db->insert([
|
||||
LanguagesTable::ID->value => $language,
|
||||
LanguagesTable::BYTES->value => $bytes
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch and add languages to total from a fully-qualified Forgejo URL
|
||||
private function add_repository_languages(string $url): void {
|
||||
foreach(self::fetch_json($url) as $language => $bytes) {
|
||||
// Create key for language if it doesn't exist
|
||||
if (!array_key_exists($language, $this->languages)) {
|
||||
$this->languages[$language] = 0;
|
||||
}
|
||||
|
||||
// Add bytes to language in total
|
||||
$this->languages[$language] += $bytes;
|
||||
}
|
||||
}
|
||||
|
||||
// Tally languages from public repositories for user id
|
||||
private function add_public_repositores(int $uid): bool {
|
||||
$resp = self::fetch_endpoint(sprintf(FORGEJO_ENDPOINT_SEARCH, $uid));
|
||||
|
||||
// Bail out if request failed or if response indicated a problem
|
||||
if (!$resp or $resp["ok"] === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add langauges for each public repository
|
||||
foreach ($resp["data"] as $repo) {
|
||||
$this->add_repository_languages($repo["languages_url"]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add languages from all public repositories for profiles in config
|
||||
private function add_repositories_from_config_profiles(): void {
|
||||
foreach(explode(",", $_ENV["server_forgejo"]["scan_profiles"]) as $profile) {
|
||||
// Resolve user data from username
|
||||
$user = self::fetch_endpoint(sprintf(FORGEJO_ENDPOINT_USER, $profile));
|
||||
$this->add_public_repositores($user["id"]);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
]);
|
||||
}
|
||||
}
|
257
install.sh
257
install.sh
|
@ -1,10 +1,251 @@
|
|||
# Install dependencies
|
||||
composer install --optimize-autoloader
|
||||
npm install
|
||||
#!/bin/bash
|
||||
|
||||
# (Re)create public NPM modules folder
|
||||
rm -r public/assets/js/modules/npm
|
||||
mkdir public/assets/js/modules/npm
|
||||
# Define constants
|
||||
DB_VLW="vlw"
|
||||
DB_API="vlw_api"
|
||||
|
||||
# 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
|
||||
# Initialize variables
|
||||
cwd=""
|
||||
database_app_host=""
|
||||
database_app_user=""
|
||||
database_seed_host=""
|
||||
database_seed_user=""
|
||||
database_app_password=""
|
||||
database_seed_password=""
|
||||
|
||||
echo_err() {
|
||||
echo "!! -> $1"
|
||||
}
|
||||
|
||||
# Make sure we have all the system packages we need to proceed with the installation
|
||||
check_sys_depend() {
|
||||
local valid=true
|
||||
local packages=("git" "composer")
|
||||
|
||||
for package in "${packages[@]}" ; do
|
||||
if ! dpkg -l | grep -qw "ii ${package}" ; then
|
||||
echo_err "Package '${package}' is not installed."
|
||||
valid=false
|
||||
fi
|
||||
done
|
||||
|
||||
# Bail out if any required package is missing
|
||||
if [ "$valid" = false ] ; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_vegvisir() {
|
||||
echo
|
||||
echo "Installing Vegvisir into '$cwd/vegvisir'"
|
||||
|
||||
curl -fsSL https://codeberg.org/vegvisir/install/raw/branch/master/install.sh | bash -s -- --install=n --overwrite=y --example=n --dir="$cwd"
|
||||
}
|
||||
|
||||
install_reflect() {
|
||||
echo
|
||||
echo "Installing Reflect into '$cwd/reflect'"
|
||||
|
||||
curl -fsSL https://codeberg.org/reflect/install/raw/branch/master/install.sh | bash -s -- --install=n --overwrite=y --seed=n --dir="$cwd" --endpoints="api" --host="$database_app_host" --user="$database_app_user" --password="$database_app_password" --db="$DB_API"
|
||||
}
|
||||
|
||||
install_vlw() {
|
||||
composer install --classmap-authoritative
|
||||
}
|
||||
|
||||
seed_databases() {
|
||||
echo
|
||||
echo "-- Database seeding --"
|
||||
echo "We're now going to seed the databases '$DB_VLW' and '$DB_API' with default data"
|
||||
echo "- Make sure that both databases exist and are empty"
|
||||
echo "- Your credentials for this user '$(whoami)' might be different from your app credentials (php-mysql)"
|
||||
echo
|
||||
|
||||
# Database seed hostname
|
||||
echo "Enter the full hostname of your MySQL/MariaDB server to use in this script for seeding."
|
||||
read -p "Press enter to use the same host as the app credentials [$database_app_host]: " database_seed_host </dev/tty
|
||||
|
||||
# Use the same database host as the app configuration
|
||||
if [[ "$database_seed_host" == "" ]] ; then
|
||||
database_seed_host=$database_app_host
|
||||
fi
|
||||
|
||||
# Database seed user
|
||||
echo
|
||||
echo "Enter the username for your MySQL/MariaDB server to use in this script for seeding."
|
||||
read -p "Press enter to use the same host as the app credentials [$database_app_user]: " database_seed_user </dev/tty
|
||||
|
||||
# Use the same database user as the app configuration
|
||||
if [[ "$database_seed_user" == "" ]] ; then
|
||||
database_seed_user=$database_app_user
|
||||
fi
|
||||
|
||||
# Database seed password
|
||||
echo
|
||||
echo "Enter the password for your MySQL/MariaDB server to use in this script for seeding."
|
||||
echo "Enter 'null' to disable password authentication"
|
||||
read -p "Press enter to use the same password as the app credentials [<database_password>]: " database_seed_password </dev/tty
|
||||
|
||||
# Use the same database password as the app configuration
|
||||
if [[ "$database_seed_password" == "" ]] ; then
|
||||
database_seed_password=$database_app_password
|
||||
fi
|
||||
|
||||
# Seed the main database
|
||||
mysql -h "$database_seed_host" -u "$database_seed_user" --password="$database_seed_password" $DB_VLW < src/Database/Seeds/vlw.sql
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo_err "Installation aborted: MySQL error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Seed the API database
|
||||
mysql -h "$database_seed_host" -u "$database_seed_user" --password="$database_seed_password" $DB_API < src/Database/Seeds/api.sql
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo_err "Installation aborted: MySQL error"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
configure_vlw() {
|
||||
local config_password=""
|
||||
|
||||
local config_available=""
|
||||
local config_available_to=""
|
||||
local config_available_from=""
|
||||
local config_available_average=""
|
||||
local config_available_timezone=""
|
||||
|
||||
local config_forgejo=""
|
||||
local config_forgejo_url=""
|
||||
local config_forgejo_profiles=""
|
||||
|
||||
# A configuration file already exists
|
||||
if [[ -f ".env.ini" ]] ; then
|
||||
echo
|
||||
echo "A configuration file already exists at: ${cwd}/.env.ini"
|
||||
read -p "Do you want to overwrite this file? (y/n): " overwrite </dev/tty
|
||||
|
||||
# Remove existing configuration file or abort
|
||||
if [[ "$overwrite" == "y" || "$overwrite" == "Y" ]] ; then
|
||||
echo "Removing existing configuration and proceeding with the installation in ${cwd}..."
|
||||
rm .env.ini
|
||||
else
|
||||
echo_err "Installation aborted: Configuration file already exists"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- Database configuration --"
|
||||
read -p "Enter the full hostname of your MySQL/MariaDB server (php-mysql): " database_app_host </dev/tty
|
||||
read -p "Enter the app username for your MySQL/MariaDB server (php-mysql): " database_app_user </dev/tty
|
||||
read -p "Enter the app password for your MySQL/MariaDB server (php-mysql): " config_password </dev/tty
|
||||
|
||||
# Coerce empty input as null keyword for later configurations
|
||||
if [[ "$config_password" == "" ]] ; then
|
||||
database_app_password="null"
|
||||
fi
|
||||
|
||||
echo
|
||||
read -p "(Optional) Would you like to configure time available settings? (y/n): " config_available </dev/tty
|
||||
|
||||
# Check the user's response
|
||||
if [[ "$config_available" == "n" || "$config_available" == "N" ]]; then
|
||||
config_available_to=0
|
||||
config_available_from=0
|
||||
config_available_average=0
|
||||
config_available_timezone="Europe/Stockholm"
|
||||
fi
|
||||
|
||||
if ! [[ -n "$config_available_timezone" ]]; then
|
||||
read -p "Enter your timezone in IANA Time Zone Database Format ('Europe/Stockholm' for example): " config_available_timezone </dev/tty
|
||||
fi
|
||||
|
||||
if ! [[ -n "$config_available_from" ]]; then
|
||||
read -p "Enter time available from hour (24-hour format): " config_available_from </dev/tty
|
||||
fi
|
||||
|
||||
if ! [[ -n "$config_available_to" ]]; then
|
||||
read -p "Enter time available to hour (24-hour format): " config_available_to </dev/tty
|
||||
fi
|
||||
|
||||
if ! [[ -n "$config_available_average" ]]; then
|
||||
read -p "Enter average reply time in hours: " config_available_average </dev/tty
|
||||
fi
|
||||
|
||||
echo
|
||||
read -p "(Optional) Would you like to configure Forgejo language updates? (y/n): " config_forgejo </dev/tty
|
||||
|
||||
# Check the user's response
|
||||
if [[ "$config_forgejo" == "n" || "$config_forgejo" == "N" ]]; then
|
||||
config_forgejo_url="https://git.vlw.se"
|
||||
config_forgejo_profiles="vlw,vegvisir,reflect"
|
||||
fi
|
||||
|
||||
if ! [[ -n "$config_forgejo_url" ]]; then
|
||||
read -p "Enter a hostname to a Forgejo instance ('https://git.vlw.se' for example): " config_forgejo_url </dev/tty
|
||||
fi
|
||||
|
||||
if ! [[ -n "$config_forgejo_profiles" ]]; then
|
||||
read -p "Enter a comma seperated list of Forgejo profiles to scan ('vlw,vegvisir,reflect' for example): " config_forgejo_profiles </dev/tty
|
||||
fi
|
||||
|
||||
local config=(
|
||||
"; This config file was generated automatically by ./install.sh"
|
||||
"; Refer to '.env.example.ini' for more information"
|
||||
"[mariadb]"
|
||||
"host = '$database_app_host'"
|
||||
"user = '$database_app_user'"
|
||||
"pass = '$config_password'"
|
||||
"db = '$DB_VLW'"
|
||||
"[config_time_available]"
|
||||
"time_zone = '$config_available_timezone'"
|
||||
"available_to_hour = '$config_available_to'"
|
||||
"reply_average_hours = '$config_available_average'"
|
||||
"available_from_hour = '$config_available_from'"
|
||||
"[service_forgejo]"
|
||||
"url = '$config_forgejo_url'"
|
||||
"profiles = '$config_forgejo_profiles'"
|
||||
)
|
||||
|
||||
for line in "${config[@]}" ; do
|
||||
echo "${line}" >> .env.ini
|
||||
done
|
||||
}
|
||||
|
||||
main() {
|
||||
# Get the current working directory
|
||||
cwd=$(pwd)
|
||||
|
||||
check_sys_depend
|
||||
|
||||
configure_vlw
|
||||
seed_databases
|
||||
|
||||
install_vlw
|
||||
install_vegvisir
|
||||
install_reflect
|
||||
|
||||
echo "-- Success --"
|
||||
echo "vlw.se has been installed! :)"
|
||||
echo "- Point all traffic to your web server to '${cwd}/vegvisir/public/index.php'"
|
||||
echo "- Point all traffic to your REST API server to '${cwd}/reflect/public/index.php'"
|
||||
echo "-------------"
|
||||
echo
|
||||
}
|
||||
|
||||
# Prompt the user for confirmation
|
||||
echo
|
||||
echo "-- Installing vlw.se --"
|
||||
echo "You are currently in: $(pwd)"
|
||||
read -p "Do you want to proceed with the installation in this directory? (y/n): " choice </dev/tty
|
||||
|
||||
# Check the user's response
|
||||
if [[ "$choice" == "y" || "$choice" == "Y" ]] ; then
|
||||
echo "Proceeding with the installation in $(pwd)..."
|
||||
main
|
||||
else
|
||||
echo "Installation aborted."
|
||||
fi
|
||||
|
|
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"elevent": "^1.0.2"
|
||||
}
|
||||
}
|
|
@ -1,26 +1,23 @@
|
|||
<?php
|
||||
|
||||
use VLW\Database\Models\Coffee\Stats;
|
||||
use VLW\Database\Models\About\Language;
|
||||
use const VLW\{
|
||||
FORGEJO_HREF,
|
||||
FORGEJO_SI_BYTE_MULTIPLE,
|
||||
DEFAULT_BUTTON_ICON
|
||||
};
|
||||
use VLW\Database\Models\Coffee\Coffee;
|
||||
use VLW\Database\Models\Languages\Language;
|
||||
|
||||
require_once VV::root("src/Consts.php");
|
||||
require_once VV::root("src/Database/Models/Coffee/Stats.php");
|
||||
require_once VV::root("src/Database/Models/About/Language.php");
|
||||
require_once VV::root("src/Database/Models/Coffee/Coffee.php");
|
||||
require_once VV::root("src/Database/Models/Languages/Language.php");
|
||||
|
||||
const FORGEJO = "https://git.vlw.se/explore/repos?language=";
|
||||
const SI_BYTE_MULTIPLE = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
|
||||
$languages = new class extends Language {
|
||||
private readonly int $total_bytes;
|
||||
|
||||
public function __construct() {
|
||||
$this->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 @@
|
|||
<stacked-bar-chart>
|
||||
|
||||
<?php foreach ($languages::all() as $language): ?>
|
||||
<a href="<?= FORGEJO_HREF . $language->id ?>" target="_blank"><chart-segment style="--size:<?= $languages->percent($language) ?>%;" data-lang="<?= $language->id ?>" data-bytes="<?= $language->bytes() ?>">
|
||||
<span data-hover><strong><?= $languages->percent_string($language) ?> <?= $language->id ?></strong><br>(<?= $language->bytes() ?> bytes)</span>
|
||||
<a href="<?= FORGEJO . $language->name ?>" target="_blank"><chart-segment style="--size:<?= $languages->percent($language) ?>%;" data-lang="<?= $language->name ?>" data-bytes="<?= $language->bytes ?>">
|
||||
<span data-hover><strong><?= $languages->percent_string($language) ?> <?= $language->name ?></strong><br>(<?= $language->bytes ?> bytes)</span>
|
||||
</chart-segment></a>
|
||||
<?php endforeach; ?>
|
||||
|
||||
|
@ -74,11 +79,11 @@
|
|||
<languages-list>
|
||||
|
||||
<?php foreach ($languages::all() as $language): ?>
|
||||
<a href="<?= FORGEJO_HREF . $language->id ?>"><button data-lang="<?= $language->id ?>" class="inline">
|
||||
<a href="<?= FORGEJO . $language->name ?>"><button data-lang="<?= $language->name ?>" class="inline">
|
||||
<p><?= $languages->percent_string($language) ?></p>
|
||||
<p class="lang"><?= $language->id ?></p>
|
||||
<p class="lang"><?= $language->name ?></p>
|
||||
<p><?= $languages->bytes_si_string($language) ?></p>
|
||||
<?= VV::embed(DEFAULT_BUTTON_ICON) ?>
|
||||
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
|
||||
</button></a>
|
||||
<?php endforeach; ?>
|
||||
|
||||
|
@ -86,8 +91,8 @@
|
|||
<stacked-bar-chart>
|
||||
|
||||
<?php foreach ($languages::all() as $language): ?>
|
||||
<a href="<?= FORGEJO_HREF . $language->id ?>" target="_blank"><chart-segment style="--size:<?= $languages->percent($language) ?>%;" data-lang="<?= $language->id ?>" data-bytes="<?= $language->bytes() ?>">
|
||||
<span data-hover><strong><?= $languages->percent_string($language) ?> <?= $language->id ?></strong><br>(<?= $language->bytes() ?> bytes)</span>
|
||||
<a href="<?= FORGEJO . $language->name ?>" target="_blank"><chart-segment style="--size:<?= $languages->percent($language) ?>%;" data-lang="<?= $language->name ?>" data-bytes="<?= $language->bytes ?>">
|
||||
<span data-hover><strong><?= $languages->percent_string($language) ?> <?= $language->name ?></strong><br>(<?= $language->bytes ?> bytes)</span>
|
||||
</chart-segment></a>
|
||||
<?php endforeach; ?>
|
||||
|
||||
|
@ -97,18 +102,18 @@
|
|||
<section class="about">
|
||||
<h2>This website</h2>
|
||||
<p>This site and all of its components, including texts and graphics have been created by me and are all <a href="https://codeberg.org/vlw/vlw.se">100% free and open source</a>. Feel free to use anything you see on this website in your own projects as long as it's under the same GNU GPLv3-or-later license. The website is designed by me on top of my own <a href="https://vegvisir.vlw.se">web framework</a> and <a href="https://reflect.vlw.se">API framework</a>.</p>
|
||||
<p>You will never find cookies or trackers on this site. The only information I have about you are in the standard NGINX web server logs, which get overwritten automatically after some time.</p>
|
||||
<p>You won't find any cookies or trackers on this site! The only information I have about you are in the standard NGINX access and error logs, which get overwritten automatically after some time.</p>
|
||||
</section>
|
||||
<section class="about">
|
||||
<h2>Personal</h2>
|
||||
<p>One thing that most people know about me is that I like coffee.. lots of coffee. In fact, I've had <?= $coffee->week() ?> cup<?= $coffee->week() === 1 ? "" : "s" ?> of coffee in the last 7 days! That's <?= $coffee->week_average_string() ?> my average of <?= $coffee->week_average() ?> per week, impressive! Even though you just read that.. I don't consider myself <i>too much</i> of a coffee snob! As long as it's dark roast and warm, I'm probably happy to have it.</p>
|
||||
<p>One thing that most people know about me is that I like coffee.. lots of coffee. In fact, I've had <?= $coffee->count_week ?> cup<?= $coffee->count_week === 1 ? "" : "s" ?> of coffee in the last 7 days! That's <?= $coffee->week_average_string() ?> my average of <?= $coffee->count_week_average ?> per week, impressive! Even though you just read that.. I don't consider myself <i>too much</i> of a coffee snob! As long as it's dark roast and warm, I'm probably happy to have it.</p>
|
||||
<p>At times, I become a true, amateur, armchair detective for a <span class="interests">variety of your typical-nerdy topics that I find interesting</span> and you can bet I spend way more time reading about those things than I will ever have use for in life.</p>
|
||||
<p>Another silent passion of mine that comes out every few years is building computers and fiddling with networking stuff.</p>
|
||||
</section>
|
||||
<hr>
|
||||
<section class="about">
|
||||
<h3>GitHub</h3>
|
||||
<p>I have <a href="https://giveupgithub.com" target="_blank" rel="noopener noreferer">given up GitHub</a> and moved most of my free software to <a href="https://codeberg.org/vlw">Codeberg</a>. You can still find my <a href="https://github.com/VictorWesterlund">GitHub profile here</a> but I don't use it for source control anymore.</p>
|
||||
<p>I have <a href="https://giveupgithub.com" target="_blank" rel="noopener noreferer">given up GitHub</a> and moved most of my free software to <a href="https://codeberg.org/vlw">Codeberg</a>. You can still find my <a href="https://github.com/VictorWesterlund">GitHub profile here</a> but I don't use it for source control of my projects anymore.</p>
|
||||
</section>
|
||||
<hr>
|
||||
<section>
|
||||
|
@ -129,7 +134,7 @@
|
|||
<p>engineering</p>
|
||||
<p>photography</p>
|
||||
<p>videography</p>
|
||||
<p>ISO 8601</p>
|
||||
<p>RFC 3339</p>
|
||||
<p>digital archiving</p>
|
||||
</div>
|
||||
<script type="module"><?= VV::js("public/assets/js/pages/about") ?></script>
|
||||
<script><?= VV::js("public/assets/js/pages/about") ?></script>
|
|
@ -106,6 +106,19 @@ section.pgp .buttons {
|
|||
gap: var(--padding);
|
||||
}
|
||||
|
||||
/* ## Blockquote */
|
||||
|
||||
cite {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
text-decoration-color: rgba(var(--primer-color-accent), .5);
|
||||
text-decoration-thickness: .1em;
|
||||
|
||||
&:hover {
|
||||
text-decoration-color: var(--color-accent);
|
||||
}
|
||||
}
|
||||
|
||||
/* ## Contact form */
|
||||
|
||||
section.form :is(input, textarea) {
|
||||
|
|
|
@ -49,6 +49,11 @@ vv-shell img {
|
|||
|
||||
.menu svg {
|
||||
width: 100%;
|
||||
animation: dash 1500ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
to { stroke-dashoffset: 32; }
|
||||
}
|
||||
|
||||
/* ### Copy email button */
|
||||
|
|
|
@ -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"));
|
||||
// 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)`);
|
||||
});
|
||||
});
|
|
@ -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"));
|
||||
}
|
||||
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)`);
|
||||
});
|
||||
});
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -1,59 +1,45 @@
|
|||
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("/"));
|
||||
|
||||
// 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?q=${event.target.value}`);
|
||||
}, DEBOUNCE_TIMEOUT_MS);
|
||||
});
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
<?php
|
||||
|
||||
use Vegvisir\Path;
|
||||
|
||||
use VLW\API\{Client, Endpoints};
|
||||
use VLW\Database\Tables\Messages\MessagesTable;
|
||||
|
||||
require_once VV::root("src/API/API.php");
|
||||
require_once VV::root("src/API/Endpoints.php");
|
||||
use VLW\Database\Models\Messages\Message;
|
||||
use VLW\Database\Tables\Messages\Messages;
|
||||
|
||||
require_once VV::root("src/Database/Models/Messages/Message.php");
|
||||
require_once VV::root("src/Database/Tables/Messages/Messages.php");
|
||||
|
||||
$date = new class extends DateTimeImmutable {
|
||||
public function __construct() {
|
||||
// Set DateTime for configured timezone
|
||||
parent::__construct("now", new DateTimeZone($_ENV["client_time_available"]["time_zone"]));
|
||||
parent::__construct("now", new DateTimeZone($_ENV["config_time_available"]["time_zone"]));
|
||||
}
|
||||
|
||||
// Return current hour in 24-hour format
|
||||
|
@ -22,20 +19,20 @@
|
|||
|
||||
// Returns true if current time is between available from and to hours
|
||||
public function is_available(): bool {
|
||||
return $this->hour() >= $_ENV["client_time_available"]["available_from_hour"] && $this->hour() < $_ENV["client_time_available"]["available_to_hour"];
|
||||
return $this->hour() >= $_ENV["config_time_available"]["available_from_hour"] && $this->hour() < $_ENV["config_time_available"]["available_to_hour"];
|
||||
}
|
||||
|
||||
public function get_estimated_reply_hours(): int {
|
||||
// I'm available! Return the estimated reply time for that
|
||||
if ($this->is_available()) {
|
||||
return $_ENV["client_time_available"]["reply_average_hours"];
|
||||
return $_ENV["config_time_available"]["reply_average_hours"];
|
||||
}
|
||||
|
||||
return $this->hour() < $_ENV["client_time_available"]["available_from_hour"]
|
||||
return $this->hour() < $_ENV["config_time_available"]["available_from_hour"]
|
||||
// Return hours past midnight until I become available (clamped to estimated reply hours)
|
||||
? max($_ENV["client_time_available"]["available_from_hour"] - $this->hour(), $_ENV["client_time_available"]["reply_average_hours"])
|
||||
? max($_ENV["config_time_available"]["available_from_hour"] - $this->hour(), $_ENV["config_time_available"]["reply_average_hours"])
|
||||
// Return hours before midnight until I become available (clamped to estimated reply hours)
|
||||
: max($_ENV["client_time_available"]["available_from_hour"] + (24 - $this->hour()), $_ENV["client_time_available"]["reply_average_hours"]);
|
||||
: max($_ENV["config_time_available"]["available_from_hour"] + (24 - $this->hour()), $_ENV["config_time_available"]["reply_average_hours"]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,9 +59,9 @@
|
|||
<?= VV::embed("public/assets/media/line.svg") ?>
|
||||
<section class="pgp">
|
||||
<?= VV::embed("public/assets/media/icons/pin.svg") ?>
|
||||
<h3>encrypt your message with PGP</h3>
|
||||
<p>Encryption is great, don't let certain <a href="https://web.archive.org/web/https://www.chatcontrol.eu">short-sighted politicians</a> fool you into thinking encryption is only for criminals. I encourage you to encrypt your message with PGP so we can aid normalization of platform-agnostic encryption for everyone. My key is available via <a href="https://wiki.gnupg.org/WKD" target="_blank" rel="noopener noreferer">WKD</a>, and it's also listed on the <a href="https://keys.openpgp.org/search?q=victor%40vlw.se" target="_blank" rel="noopener noreferer">openPGP key server</a>.</p>
|
||||
<p>Here's the fingerprint for victor@vlw.se<br><strong class="fingerprint">DCE987311CB5D2A252F58951D0AD730E1057DFC6</strong></p>
|
||||
<h3>a note about encryption..</h3>
|
||||
<p>Please don't let certain <a href="https://web.archive.org/web/20250514040615/https://www.patrick-breyer.de/en/posts/chat-control/">short-sighted politicians</a> fool you into thinking encrypted text messages are only for <a href="https://en.wikipedia.org/wiki/Nothing_to_hide_argument">"those with nothing to hide"</a>. I encourage you to encrypt your message with PGP so we can aid normalization of <i>platform-agnostic</i> encryption for everyone.</p>
|
||||
<p>PGP fingerprint for victor@vlw.se:<br><strong class="fingerprint">DCE987311CB5D2A252F58951D0AD730E1057DFC6</strong></p>
|
||||
<div class="buttons">
|
||||
<a href="https://keys.openpgp.org/vks/v1/by-fingerprint/DCE987311CB5D2A252F58951D0AD730E1057DFC6"><button class="inline solid download">
|
||||
<p>download ASC</p>
|
||||
|
@ -77,25 +74,16 @@
|
|||
</div>
|
||||
</section>
|
||||
<section class="center fade">
|
||||
<p>And for reference, here is the fingerprint for my "non-personal" address info@vlw.se</p>
|
||||
<p><strong class="fingerprint">DC603DA049903D707B7F1DB39AF727FB576F5A00</strong></p>
|
||||
<blockquote>"Arguing that you don't care about the right to privacy because you have nothing to hide is no different than saying you don't care about free speech because you have nothing to say."</blockquote>
|
||||
<p>- Edward Snowden, <cite><a href="https://en.wikiquote.org/wiki/Edward_Snowden#2015">Reddit (May 21, 2015)</a></cite></p>
|
||||
</section>
|
||||
<?= VV::embed("public/assets/media/line.svg") ?>
|
||||
|
||||
<?php // Send message on POST request ?>
|
||||
<?php if ($_SERVER["REQUEST_METHOD"] === "POST"): ?>
|
||||
<?php $message = Message::new($_POST[Messages::MESSAGE->name] ?? "", $_POST[Messages::EMAIL->name] ?? null); ?>
|
||||
|
||||
<?php
|
||||
|
||||
// Send message via API
|
||||
$send = (new Client())->call(Endpoints::MESSAGES->value)->post([
|
||||
MessagesTable::EMAIL->value => $_POST[MessagesTable::EMAIL->value],
|
||||
MessagesTable::MESSAGE->value => $_POST[MessagesTable::MESSAGE->value]
|
||||
]);
|
||||
|
||||
?>
|
||||
|
||||
<?php if ($send->ok): ?>
|
||||
<?php if ($message->date_created): ?>
|
||||
<section class="form-message sent">
|
||||
<h3>🙏 Message sent!</h3>
|
||||
</section>
|
||||
|
@ -103,8 +91,6 @@
|
|||
<?php // Show response body from endpoint as error if request failed ?>
|
||||
<section class="form-message error">
|
||||
<h3>😟 Oh no, something went wrong</h3>
|
||||
<p>Response from API:</p>
|
||||
<pre><?= $send->output() ?></pre>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
@ -113,11 +99,11 @@
|
|||
<form method="POST">
|
||||
<input-group>
|
||||
<label>your email (optional)</label>
|
||||
<input type="email" name="<?= MessagesTable::EMAIL->value ?>" placeholder="nissehult@example.com" autocomplete="off"></input>
|
||||
<input type="email" name="<?= Messages::EMAIL->name ?>" placeholder="nissehult@example.com" autocomplete="off"></input>
|
||||
</input-group>
|
||||
<input-group>
|
||||
<label title="this field is required">your message (required)</label>
|
||||
<textarea name="<?= MessagesTable::MESSAGE->value ?>" required placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed molestie dignissim mauris vel dignissim. Sed et aliquet odio, id egestas libero. Vestibulum ut dui a turpis aliquam hendrerit id et dui. Morbi eu tristique quam, sit amet dictum felis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ac nibh a ex accumsan ullamcorper non quis eros. Nam at suscipit lacus. Nullam placerat semper sapien, vitae aliquet nisl elementum a. Duis viverra quam eros, eu vestibulum quam egestas sit amet. Duis lobortis varius malesuada. Mauris in fringilla mi. "></textarea>
|
||||
<textarea name="<?= Messages::MESSAGE->name ?>" required placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed molestie dignissim mauris vel dignissim. Sed et aliquet odio, id egestas libero. Vestibulum ut dui a turpis aliquam hendrerit id et dui. Morbi eu tristique quam, sit amet dictum felis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ac nibh a ex accumsan ullamcorper non quis eros. Nam at suscipit lacus. Nullam placerat semper sapien, vitae aliquet nisl elementum a. Duis viverra quam eros, eu vestibulum quam egestas sit amet. Duis lobortis varius malesuada. Mauris in fringilla mi. "></textarea>
|
||||
</input-group>
|
||||
<button class="inline solid">
|
||||
<?= VV::embed("public/assets/media/icons/email.svg") ?>
|
||||
|
@ -126,5 +112,4 @@
|
|||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script type="module"><?= VV::js("public/assets/js/pages/contact") ?></script>
|
||||
<script ><?= VV::js("public/assets/js/pages/contact") ?></script>
|
|
@ -1,26 +1,23 @@
|
|||
<?php
|
||||
|
||||
use VLW\Database\Models\Search\Search;
|
||||
use const VLW\{ICONS_DIR, DEFAULT_BUTTON_ICON};
|
||||
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/Database/Tables/Search/Search.php");
|
||||
require_once VV::root("src/Database/Models/Search/Search.php");
|
||||
|
||||
const GET_KEY_QUERY = "q";
|
||||
const LIMIT_RESULTS = 10;
|
||||
const MIN_QUERY_LENGTH = 2;
|
||||
|
||||
$search = new class extends Search {
|
||||
public function __construct() {}
|
||||
|
||||
public static function get_query(): ?string {
|
||||
return $_GET[SearchTable::QUERY->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 = strlen($this->query) >= MIN_QUERY_LENGTH ? parent::query($this->query, limit: LIMIT_RESULTS) : [];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,54 +25,54 @@
|
|||
<style><?= VV::css("public/assets/css/pages/search") ?></style>
|
||||
<section class="search">
|
||||
<form>
|
||||
<input name="<?= SearchTable::QUERY->value ?>" type="search" placeholder="search vlw.se..." value="<?= $search::get_query() ?>">
|
||||
<select name="<?= SearchTable::CATEGORY->value ?>">
|
||||
<input name="<?= GET_KEY_QUERY ?>" type="search" placeholder="search vlw.se..." value="<?= $search->query ?>">
|
||||
<select name="<?= SearchTable::TYPE->value ?>">
|
||||
<option value="null">All</option>
|
||||
<optgroup label="Categories">
|
||||
<optgroup label="Types">
|
||||
|
||||
<?php foreach (SearchCategoryEnum::names() as $category): ?>
|
||||
<?php $category = SearchCategoryEnum::fromName($category); ?>
|
||||
<option value="<?= $category->name ?>" <?= $search::get_category() === $category ? "selected" : "" ?>><?= ucfirst(strtolower($category->name)) ?></option>
|
||||
<?php foreach (SearchTypeEnum::names() as $type): ?>
|
||||
<?php $type = SearchTypeEnum::fromName($type); ?>
|
||||
<option value="<?= $type->name ?>"><?= ucfirst(strtolower($type->name)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</optgroup>
|
||||
</select>
|
||||
<button type="submit" class="inline solid"><?= VV::embed(ICONS_DIR . "search.svg") ?></button>
|
||||
<button type="submit" class="inline solid"><?= VV::embed("public/assets/media/icons/search.svg") ?></button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<?php if (array_key_exists(SearchTable::QUERY->value, $_GET)): ?>
|
||||
<?php if (isset($_GET[GET_KEY_QUERY])): ?>
|
||||
|
||||
<?php if ($search->search()): ?>
|
||||
<?php if ($search->results): ?>
|
||||
<section class="stats">
|
||||
<p><?= count($search->search()) ?> result(s)</p>
|
||||
<a href="/search?query=<?= $search::get_query() ?>"><button class="inline solid">
|
||||
<?= VV::embed(ICONS_DIR . "search.svg") ?>
|
||||
<p><?= count($search->results) ?> result(s)</p>
|
||||
<a href="/search?query=<?= $search->query ?>"><button class="inline solid">
|
||||
<?= VV::embed("public/assets/media/icons/search.svg") ?>
|
||||
<p>Advanced search</p>
|
||||
<?= VV::embed(DEFAULT_BUTTON_ICON) ?>
|
||||
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
|
||||
</button></a>
|
||||
</section>
|
||||
|
||||
<?php foreach ($search->search() as $result): ?>
|
||||
<?php foreach ($search->results as $result): ?>
|
||||
<section class="result" data-id="<?= $result->id ?>">
|
||||
<a href="<?= $result->href() ?>"><button class="inline">
|
||||
<a href="<?= $result->href ?>"><button class="inline">
|
||||
<div>
|
||||
<h2><?= $result->title() ?></h2>
|
||||
<p><?= $result->summary() ?></p>
|
||||
<h3><?= $result->title ?></h3>
|
||||
<p><?= $result->text ?></p>
|
||||
</div>
|
||||
<?= VV::embed(DEFAULT_BUTTON_ICON) ?>
|
||||
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
|
||||
</button></a>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php else: ?>
|
||||
<?php switch (strlen($search::get_query())): default: ?>
|
||||
<?php switch (strlen($search->query)): default: ?>
|
||||
<section class="stats">
|
||||
<p>0 result(s)</p>
|
||||
<a href="/search?query=<?= $search::get_query() ?>"><button class="inline solid">
|
||||
<?= VV::embed(ICONS_DIR . "search.svg") ?>
|
||||
<a href="/search?query=<?= $search->query ?>"><button class="inline solid">
|
||||
<?= VV::embed("public/assets/media/icons/search.svg") ?>
|
||||
<p>Advanced search</p>
|
||||
<?= VV::embed(DEFAULT_BUTTON_ICON) ?>
|
||||
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
|
||||
</button></a>
|
||||
</section>
|
||||
<section class="center">
|
||||
|
@ -86,15 +83,15 @@
|
|||
|
||||
<?php case 0: ?>
|
||||
<section class="center">
|
||||
<?= VV::embed(ICONS_DIR . "search.svg") ?>
|
||||
<?= VV::embed("public/assets/media/icons/search.svg") ?>
|
||||
<p>Start typing to search</p>
|
||||
</section>
|
||||
<?php break; ?>
|
||||
|
||||
<?php case 1: ?>
|
||||
<?php case (MIN_QUERY_LENGTH - 1): ?>
|
||||
<section class="center">
|
||||
<?= VV::embed(ICONS_DIR . "search.svg") ?>
|
||||
<p>Almost, type at least two letters to search</p>
|
||||
<?= VV::embed("public/assets/media/icons/search.svg") ?>
|
||||
<p>Almost there, type at least two letters to search</p>
|
||||
</section>
|
||||
<?php break; ?>
|
||||
|
||||
|
@ -104,7 +101,7 @@
|
|||
|
||||
<?php else: ?>
|
||||
<section class="center">
|
||||
<?= VV::embed(ICONS_DIR . "search.svg") ?>
|
||||
<?= VV::embed("public/assets/media/icons/search.svg") ?>
|
||||
<p>Start typing to search</p>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
@ -137,7 +135,7 @@
|
|||
<section class="heading">
|
||||
<h1>latest projects</h1>
|
||||
</section>
|
||||
<?= VV::include("public/work/timeline?" . http_build_query([TIMELINE_PREVIEW_LIMIT_PARAM => TIMELINE_PREVIEW_LIMIT_COUNT])) ?>
|
||||
<?= VV::include("public/work/timeline", limit: 4) ?>
|
||||
<section class="heading">
|
||||
<a href="/work/timeline"><button class="inline solid">
|
||||
<p>view full timeline</p>
|
|
@ -1,42 +1,35 @@
|
|||
<?php
|
||||
|
||||
use VLW\Database\Models\Work\Timeline;
|
||||
use const VLW\{
|
||||
ICONS_DIR,
|
||||
DEFAULT_BUTTON_ICON,
|
||||
TIMELINE_PREVIEW_LIMIT_PARAM
|
||||
};
|
||||
|
||||
require_once VV::root("src/Consts.php");
|
||||
require_once VV::root("src/Database/Models/Work/Timeline.php");
|
||||
|
||||
const ARG_KEY_LIMIT = "limit";
|
||||
|
||||
$timeline = new class extends Timeline {
|
||||
public function __construct() {}
|
||||
|
||||
public static function ordered(): array {
|
||||
// Get timeline list limit from search param if set
|
||||
$limit = array_key_exists(TIMELINE_PREVIEW_LIMIT_PARAM, $_GET) ? (int) $_GET[TIMELINE_PREVIEW_LIMIT_PARAM] : null;
|
||||
|
||||
public static function ordered(?int $limit = null): array {
|
||||
$timeline = [];
|
||||
|
||||
foreach (parent::all() as $idx => $item) {
|
||||
// Use year as the first dimension
|
||||
if (!array_key_exists($item->year(), $timeline)) {
|
||||
$timeline[$item->year()] = [];
|
||||
if (!array_key_exists($item->year, $timeline)) {
|
||||
$timeline[$item->year] = [];
|
||||
}
|
||||
|
||||
// And month as the second dimension
|
||||
if (!array_key_exists($item->month(), $timeline[$item->year()])) {
|
||||
$timeline[$item->year()][$item->month()] = [];
|
||||
if (!array_key_exists($item->month, $timeline[$item->year])) {
|
||||
$timeline[$item->year][$item->month] = [];
|
||||
}
|
||||
|
||||
// Lastly, day as the third dimension
|
||||
if (!array_key_exists($item->day(), $timeline[$item->year()][$item->month()])) {
|
||||
$timeline[$item->year()][$item->month()][$item->day()] = [];
|
||||
if (!array_key_exists($item->day, $timeline[$item->year][$item->month])) {
|
||||
$timeline[$item->year][$item->month][$item->day] = [];
|
||||
}
|
||||
|
||||
// Append Work instance on Timeline object to the output array by year->month->day
|
||||
$timeline[$item->year()][$item->month()][$item->day()][] = $item->work();
|
||||
$timeline[$item->year][$item->month][$item->day][] = $item->work;
|
||||
|
||||
// Bail out here if we've reached the theshold for items to display
|
||||
if ($limit && $idx === $limit) {
|
||||
|
@ -58,7 +51,7 @@
|
|||
<p>Codeberg</p>
|
||||
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
|
||||
</button></a>
|
||||
<a href="https://git.vlw.se"><button class="inline">
|
||||
<a href="https://git.vlw.se/explore/repos"><button class="inline">
|
||||
<p>Forgejo</p>
|
||||
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
|
||||
</button></a>
|
||||
|
@ -67,7 +60,7 @@
|
|||
<section class="timeline">
|
||||
|
||||
<?php // Get year int from key and array of months for current year ?>
|
||||
<?php foreach ($timeline::ordered() as $year => $months): ?>
|
||||
<?php foreach ($timeline::ordered($args[ARG_KEY_LIMIT] ?? null) as $year => $months): ?>
|
||||
<div class="year">
|
||||
<div class="track">
|
||||
<p><?= $year ?></p>
|
||||
|
@ -100,33 +93,33 @@
|
|||
<div class="tags">
|
||||
|
||||
<?php foreach ($work->tags() as $tag): ?>
|
||||
<p class="tag <?= $tag->label()->name ?>"><?= $tag->label()->name ?></p>
|
||||
<p class="tag <?= $tag->label->name ?>"><?= $tag->label->name ?></p>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($work->title()): ?>
|
||||
<h2><?= $work->title() ?></h2>
|
||||
<?php if ($work->title): ?>
|
||||
<h2><?= $work->title ?></h2>
|
||||
<?php endif; ?>
|
||||
|
||||
<p><?= $work->summary() ?></p>
|
||||
<p><?= $work->summary ?></p>
|
||||
|
||||
<?php if ($work->actions()): ?>
|
||||
<div class="actions">
|
||||
|
||||
<?php foreach ($work->actions() as $action): ?>
|
||||
<a href="<?= $action->href() ?? "/work/{$work->id}" ?>"><button class="inline <?= implode(" ", $action->classes()) ?>">
|
||||
<?php if ($action->icon_prepended()): ?>
|
||||
<?= VV::embed(ICONS_DIR . $action->icon_prepended()) ?>
|
||||
<a href="<?= $action->href ?? "/work/{$work->id}" ?>"><button class="inline <?= $action->classlist ?>">
|
||||
<?php if ($action->icon_prepend): ?>
|
||||
<?= VV::embed("public/assets/media/icons/" . $action->icon_prepend) ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<p><?= $action->display_text() ?></p>
|
||||
<p><?= $action->text ?></p>
|
||||
|
||||
<?php if ($action->icon_appended()): ?>
|
||||
<?= VV::embed(ICONS_DIR . $action->icon_appended()) ?>
|
||||
<?php if ($action->icon_append): ?>
|
||||
<?= VV::embed("public/assets/media/icons/" . $action->icon_append) ?>
|
||||
<?php else: ?>
|
||||
<?= VV::embed(DEFAULT_BUTTON_ICON) ?>
|
||||
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
|
||||
<?php endif; ?>
|
||||
</button></a>
|
||||
<?php endforeach; ?>
|
||||
|
@ -149,5 +142,4 @@
|
|||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</section>
|
||||
<script><?= VV::js("assets/js/pages/work/timeline") ?></script>
|
||||
</section>
|
|
@ -1,10 +1,3 @@
|
|||
<?php
|
||||
|
||||
use const VLW\DEFAULT_BUTTON_ICON;
|
||||
|
||||
require_once VV::root("src/Consts.php");
|
||||
|
||||
?>
|
||||
<style><?= VV::css("public/assets/css/pages/work/wip") ?></style>
|
||||
<section class="disclaimer">
|
||||
<h1>Soon, very soon!</h1>
|
||||
|
@ -13,6 +6,6 @@
|
|||
<section class="actions">
|
||||
<a href="/work"><button class="inline">
|
||||
<p>to featured work</p>
|
||||
<?= VV::embed(DEFAULT_BUTTON_ICON) ?>
|
||||
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
|
||||
</button></a>
|
||||
</section>
|
1
reflect
Submodule
1
reflect
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit f8d45950d78a16adc64db920b5dbf59dabce5bca
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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
|
||||
|
@ -27,7 +13,4 @@
|
|||
* # Forgejo
|
||||
* Constants related to the fetching and caching of real-time prog. language use on Forgejo
|
||||
*/
|
||||
const FORGEJO_HREF = "https://git.vlw.se/explore/repos?q=&sort=recentupdate&language=";
|
||||
const FORGEJO_ENDPOINT_USER = "/api/v1/users/%s";
|
||||
const FORGEJO_ENDPOINT_SEARCH = "/api/v1/repos/search?uid=%s";
|
||||
const FORGEJO_SI_BYTE_MULTIPLE = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -3,33 +3,74 @@
|
|||
namespace VLW\Database\Models\Coffee;
|
||||
|
||||
use \VV;
|
||||
use \Exception;
|
||||
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\Coffee\CoffeeTable;
|
||||
use VLW\Database\Tables\Coffee\{Stats, Coffee as CoffeeTable};
|
||||
|
||||
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/Coffee/Stats.php");
|
||||
require_once VV::root("src/Database/Tables/Coffee/Coffee.php");
|
||||
require_once VV::root("src/Database/Models/Coffee/Coffee.php");
|
||||
|
||||
class Coffee extends Model {
|
||||
final public static function new(?DateTimeImmutable $datetime = null): self {
|
||||
$id = UUID::v4();
|
||||
|
||||
if (!parent::create(CoffeeTable::TABLE, [
|
||||
CoffeeTable::ID->value => $id,
|
||||
CoffeeTable::DATE_CREATED->value => $datetime ? $datetime->format(parent::DATE_FORMAT) : date(parent::DATE_FORMAT)
|
||||
])) { throw new Exception("Failed to create Work entity"); }
|
||||
|
||||
return new Coffee($id);
|
||||
}
|
||||
|
||||
final public static function all(): array {
|
||||
return array_map(fn(array $work): Coffee => new Coffee($work[CoffeeTable::ID->value]), new Database()
|
||||
->from(CoffeeTable::TABLE)
|
||||
->order([CoffeeTable::DATE_CREATED->value => Order::DESC])
|
||||
->select(CoffeeTable::ID->value)
|
||||
->fetch_all(MYSQLI_ASSOC)
|
||||
);
|
||||
}
|
||||
|
||||
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(Endpoints::COFFEE, [
|
||||
parent::__construct(CoffeeTable::TABLE, CoffeeTable::values(), [
|
||||
CoffeeTable::ID->value => $this->id
|
||||
]);
|
||||
}
|
||||
|
||||
public static function all(array $params = []): array {
|
||||
return array_map(fn(array $item): Coffee => new Coffee($item[CoffeeTable::ID->value]), parent::list(Endpoints::COFFEE, $params));
|
||||
public function delete(): bool {
|
||||
try {
|
||||
return $this->db->from(CoffeeTable::TABLE)->delete([CoffeeTable::ID->value => $this->id]);
|
||||
} catch (Exception $error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function timestamp(): int {
|
||||
return $this->get(CoffeeTable::ID->value);
|
||||
}
|
||||
|
||||
public function datetime(): DateTimeImmutable {
|
||||
return DateTimeImmutable::createFromFormat("U", $this->timestamp());
|
||||
final public DateTimeImmutable $date_created {
|
||||
get => new DateTimeImmutable($this->get(CoffeeTable::DATE_CREATED->value));
|
||||
set (DateTimeImmutable $date_created) => $this->set(CoffeeTable::DATE_CREATED->value, $date_created->format(parent::DATE_FORMAT));
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace VLW\Database\Models\Coffee;
|
||||
|
||||
use \VV;
|
||||
|
||||
use VLW\API\Endpoints;
|
||||
use VLW\Database\Models\Model;
|
||||
use VLW\Database\Tables\Coffee\StatsTable;
|
||||
|
||||
require_once VV::root("src/API/Endpoints.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/Models/Coffee/Stats.php");
|
||||
|
||||
class Stats extends Model {
|
||||
public function __construct() {
|
||||
parent::__construct(Endpoints::COFFEE_STATS);
|
||||
}
|
||||
|
||||
public static function all(array $params = []): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function week(): int {
|
||||
return $this->get(StatsTable::COUNT_WEEK->value) ?? 0;
|
||||
}
|
||||
|
||||
public function week_average(): int {
|
||||
return $this->get(StatsTable::COUNT_WEEK_AVERAGE->value) ?? 0;
|
||||
}
|
||||
}
|
65
src/Database/Models/Languages/Language.php
Normal file
65
src/Database/Models/Languages/Language.php
Normal file
|
@ -0,0 +1,65 @@
|
|||
<?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\Languages\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 all(): array {
|
||||
return array_map(fn(array $language): Language => new Language($language[Languages::ID->value]), new Database()
|
||||
->from(Languages::TABLE)
|
||||
->order([Languages::BYTES->value => Order::DESC])
|
||||
->select(Languages::ID->value)
|
||||
->fetch_all(MYSQLI_ASSOC)
|
||||
);
|
||||
}
|
||||
|
||||
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 string $name {
|
||||
get => $this->get(Languages::NAME->value);
|
||||
set (string $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);
|
||||
}
|
||||
}
|
62
src/Database/Models/Messages/Message.php
Normal file
62
src/Database/Models/Messages/Message.php
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace VLW\Database\Models\Messages;
|
||||
|
||||
use \VV;
|
||||
use \vlw\MySQL\Order;
|
||||
use \DateTimeImmutable;
|
||||
|
||||
use VLW\Helpers\UUID;
|
||||
use VLW\Database\Database;
|
||||
use VLW\Database\Models\Model;
|
||||
use VLW\Database\Tables\Messages\Messages;
|
||||
|
||||
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/Messages/Messages.php");
|
||||
|
||||
class Message extends Model {
|
||||
final public static function new(string $message, ?string $email = null): self {
|
||||
$id = UUID::v4();
|
||||
|
||||
if (!parent::create(Messages::TABLE, [
|
||||
Messages::ID->value => $id,
|
||||
Messages::EMAIL->value => $email,
|
||||
Messages::MESSAGE->value => $message,
|
||||
Messages::DATE_CREATED->value => date(parent::DATE_FORMAT)
|
||||
])) { throw new Exception("Failed to create Message entity"); }
|
||||
|
||||
return new Message($id);
|
||||
}
|
||||
|
||||
final public static function all(): array {
|
||||
return array_map(fn(array $Message): Message => new Message($Message[Messages::ID->value]), new Database()
|
||||
->from(Messages::TABLE)
|
||||
->order([Messages::DATE_CREATED->value => Order::DESC])
|
||||
->select(Messages::ID->value)
|
||||
->fetch_all(MYSQLI_ASSOC)
|
||||
);
|
||||
}
|
||||
|
||||
public function __construct(public readonly string $id) {
|
||||
parent::__construct(Messages::TABLE, Messages::values(), [
|
||||
Messages::ID->value => $this->id
|
||||
]);
|
||||
}
|
||||
|
||||
final public ?string $email {
|
||||
get => $this->get(Messages::EMAIL->value);
|
||||
set (?string $email) => $this->set(Messages::EMAIL->value, $email);
|
||||
}
|
||||
|
||||
final public string $message {
|
||||
get => $this->get(Messages::MESSAGE->value);
|
||||
set (string $message) => $this->set(Messages::MESSAGE->value, $message);
|
||||
}
|
||||
|
||||
final public DateTimeImmutable $date_created {
|
||||
get => new DateTimeImmutable($this->get(Messages::DATE_CREATED->value));
|
||||
set (DateTimeImmutable $date_created) => $this->set(Messages::DATE_CREATED->value, $date_created->format(parent::DATE_FORMAT));
|
||||
}
|
||||
}
|
|
@ -4,40 +4,59 @@
|
|||
|
||||
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; }
|
||||
|
||||
private static Database $_db;
|
||||
|
||||
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)));
|
||||
// Establish once and reuse Database connection
|
||||
$this->db = self::$_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]);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -3,49 +3,84 @@
|
|||
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\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/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");
|
||||
|
||||
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): Action => new Action($tag[Actions::ID->value]), new Database()
|
||||
->from(Actions::TABLE)
|
||||
->where([Actions::REF_WORK_ID->value => $work->id])
|
||||
->order([Actions::ORDER_IDX->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 $text {
|
||||
get => $this->get(Actions::TEXT->value);
|
||||
set (string $text) => $this->set(Actions::TEXT->value, $text);
|
||||
}
|
||||
|
||||
public function classes(): array {
|
||||
return $this->get(ActionsTable::CLASS_LIST->value) ? explode(",", $this->get(ActionsTable::CLASS_LIST->value)) : [];
|
||||
final public ?string $classlist {
|
||||
get => $this->get(Actions::CLASSLIST->value);
|
||||
set (?string $classlist) => $this->set(Actions::CLASSLIST->value, $classlist);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -3,41 +3,71 @@
|
|||
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\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/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");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
final public static function all(): array {
|
||||
return array_map(fn(array $work): Timeline => new Timeline($work[TimelineTable::ID->value]), new Database()
|
||||
->from(TimelineTable::TABLE)
|
||||
->order([
|
||||
TimelineTable::YEAR->value => Order::DESC,
|
||||
TimelineTable::MONTH->value => Order::DESC,
|
||||
TimelineTable::DAY->value => Order::DESC
|
||||
])
|
||||
->select(TimelineTable::ID->value)
|
||||
->fetch_all(MYSQLI_ASSOC)
|
||||
);
|
||||
}
|
||||
|
||||
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 => new Work($this->get(TimelineTable::REF_WORK_ID->value));
|
||||
set (Work $work) => $this->set(TimelineTable::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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -14,24 +14,22 @@ CREATE TABLE `acl` (
|
|||
`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
|
||||
('about/languages', 1),
|
||||
('coffee', 1),
|
||||
('coffee/stats', 1),
|
||||
('messages', 1),
|
||||
('notes', 1),
|
||||
('playground/coffee', 1),
|
||||
('search', 1),
|
||||
('languages', 1),
|
||||
('update', 1),
|
||||
('work', 1),
|
||||
('work/actions', 1),
|
||||
('work/tags', 1),
|
||||
('work/timeline', 1);
|
||||
('work', 1);
|
||||
|
||||
CREATE TABLE `groups` (
|
||||
`id` varchar(255) NOT NULL,
|
|
@ -8,33 +8,29 @@ 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
|
||||
CREATE TRIGGER `coffee_stats_update` 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);
|
||||
WHERE date_created > 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)))
|
||||
|
||||
SELECT COUNT(*) / COUNT(DISTINCT YEAR(date_created), WEEK(date_created))
|
||||
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
|
||||
$$
|
||||
|
@ -43,241 +39,93 @@ 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;
|
||||
) ENGINE=InnoDB 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;
|
||||
`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) DEFAULT NULL
|
||||
) ENGINE=InnoDB 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
|
||||
`text` varchar(255) NOT 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 `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`);
|
||||
|
||||
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`
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace VLW\Database\Tables\About;
|
||||
|
||||
use vlw\xEnum;
|
||||
|
||||
enum LanguagesTable: string {
|
||||
use xEnum;
|
||||
|
||||
const NAME = "about_languages";
|
||||
|
||||
case ID = "id";
|
||||
case BYTES = "bytes";
|
||||
}
|
|
@ -4,10 +4,11 @@
|
|||
|
||||
use vlw\xEnum;
|
||||
|
||||
enum CoffeeTable: string {
|
||||
enum Coffee: string {
|
||||
use xEnum;
|
||||
|
||||
const NAME = "coffee";
|
||||
const TABLE = "coffee";
|
||||
|
||||
case ID = "id";
|
||||
case ID = "id";
|
||||
case DATE_CREATED = "date_created";
|
||||
}
|
|
@ -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";
|
||||
|
|
15
src/Database/Tables/Languages/Languages.php
Normal file
15
src/Database/Tables/Languages/Languages.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace VLW\Database\Tables\Languages;
|
||||
|
||||
use vlw\xEnum;
|
||||
|
||||
enum Languages: string {
|
||||
use xEnum;
|
||||
|
||||
const TABLE = "languages";
|
||||
|
||||
case ID = "id";
|
||||
case NAME = "name";
|
||||
case BYTES = "bytes";
|
||||
}
|
|
@ -2,10 +2,15 @@
|
|||
|
||||
namespace VLW\Database\Tables\Messages;
|
||||
|
||||
enum MessagesTable: string {
|
||||
const NAME = "messages";
|
||||
use vlw\xEnum;
|
||||
|
||||
case EMAIL = "email";
|
||||
case MESSAGE = "message";
|
||||
case TIMESTAMP_CREATED = "timestamp_created";
|
||||
enum Messages: string {
|
||||
use xEnum;
|
||||
|
||||
const TABLE = "messages";
|
||||
|
||||
case ID = "id";
|
||||
case EMAIL = "email";
|
||||
case MESSAGE = "message";
|
||||
case DATE_CREATED = "date_created";
|
||||
}
|
|
@ -4,21 +4,21 @@
|
|||
|
||||
use vlw\xEnum;
|
||||
|
||||
enum SearchCategoryEnum {
|
||||
enum SearchTypeEnum {
|
||||
use xEnum;
|
||||
|
||||
case WORK;
|
||||
}
|
||||
|
||||
enum SearchTable: string {
|
||||
enum Search: string {
|
||||
use xEnum;
|
||||
|
||||
const NAME = "search";
|
||||
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";
|
||||
}
|
|
@ -3,17 +3,18 @@
|
|||
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 TEXT = "text";
|
||||
case CLASSLIST = "classlist";
|
||||
case ICON_PREPEND = "icon_prepend";
|
||||
case ICON_APPEND = "icon_append";
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace VLW\Database\Tables\Work;
|
||||
|
||||
enum MediaTable: string {
|
||||
const NAME = "work_media";
|
||||
|
||||
case ANCHOR = "anchor";
|
||||
case MEDIA = "media";
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
}
|
95
src/Helpers/Forgejo.php
Normal file
95
src/Helpers/Forgejo.php
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace VLW\Helpers;
|
||||
|
||||
use \VV;
|
||||
|
||||
use VLW\Database\Database;
|
||||
use VLW\Database\Models\Languages\Language;
|
||||
use VLW\Database\Tables\Languages\Languages;
|
||||
|
||||
require_once VV::root("src/Database/Database.php");
|
||||
require_once VV::root("src/Database/Models/Languages/Language.php");
|
||||
require_once VV::root("src/Database/Tables/Languages/Languages.php");
|
||||
|
||||
class Forgejo {
|
||||
private const FORGEJO_HREF = "https://git.vlw.se/explore/repos?q=&sort=recentupdate&language=";
|
||||
private const FORGEJO_ENDPOINT_USER = "/api/v1/users/%s";
|
||||
private const FORGEJO_ENDPOINT_SEARCH = "/api/v1/repos/search?uid=%s";
|
||||
|
||||
private array $languages;
|
||||
private readonly Database $db;
|
||||
|
||||
// Fetch JSON from URL
|
||||
private static function fetch_json(string $url): array {
|
||||
return json_decode(file_get_contents($url), true);
|
||||
}
|
||||
|
||||
// Fetch JSON from a Forgejo endpoint
|
||||
private static function fetch_endpoint(string $endpoint): array {
|
||||
$url = $_ENV["service_forgejo"]["url"] . $endpoint;
|
||||
return self::fetch_json($url);
|
||||
}
|
||||
|
||||
public function __construct() {
|
||||
$this->db = new Database();
|
||||
$this->languages = [];
|
||||
}
|
||||
|
||||
// Add languages from all public repositories for profiles in config
|
||||
public function update(): bool {
|
||||
foreach(explode(",", $_ENV["service_forgejo"]["profiles"]) as $profile) {
|
||||
// Resolve user data from username
|
||||
$user = self::fetch_endpoint(sprintf(self::FORGEJO_ENDPOINT_USER, $profile));
|
||||
if (!$this->add_public_repositores($user["id"])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->save_languages();
|
||||
return true;
|
||||
}
|
||||
|
||||
private function truncate(): bool {
|
||||
return $this->db->from(Languages::TABLE)->delete();
|
||||
}
|
||||
|
||||
// Save languages from $this->languages to the database
|
||||
private function save_languages(): void {
|
||||
$this->truncate();
|
||||
|
||||
foreach ($this->languages as $language => $bytes) {
|
||||
Language::new($language, $bytes);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch and add languages to total from a fully-qualified Forgejo URL
|
||||
private function add_repository_languages(string $url): void {
|
||||
foreach(self::fetch_json($url) as $language => $bytes) {
|
||||
// Create key for language if it doesn't exist
|
||||
if (!array_key_exists($language, $this->languages)) {
|
||||
$this->languages[$language] = 0;
|
||||
}
|
||||
|
||||
// Add bytes to language in total
|
||||
$this->languages[$language] += $bytes;
|
||||
}
|
||||
}
|
||||
|
||||
// Tally languages from public repositories for user id
|
||||
private function add_public_repositores(int $uid): bool {
|
||||
$resp = self::fetch_endpoint(sprintf(self::FORGEJO_ENDPOINT_SEARCH, $uid));
|
||||
|
||||
// Bail out if request failed or if response indicated a problem
|
||||
if (!$resp or $resp["ok"] === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add langauges for each public repository
|
||||
foreach ($resp["data"] as $repo) {
|
||||
$this->add_repository_languages($repo["languages_url"]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
56
src/Helpers/GenerateSearch.php
Normal file
56
src/Helpers/GenerateSearch.php
Normal 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;
|
||||
}
|
||||
}
|
36
src/Helpers/GenerateTimeline.php
Normal file
36
src/Helpers/GenerateTimeline.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?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) {
|
||||
if (!Timeline::new($work)) { return false; };
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function truncate(): bool {
|
||||
return $this->db->from(TimelineTable::TABLE)->delete();
|
||||
}
|
||||
}
|
25
src/Helpers/UUID.php
Normal file
25
src/Helpers/UUID.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace VLW\Helpers;
|
||||
|
||||
class UUID {
|
||||
public const LENGTH = 36;
|
||||
|
||||
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
1
vegvisir
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 016b88068212243ce33894fbba9ffa91009146f0
|
Loading…
Add table
Reference in a new issue