Compare commits

..

1 commit

151 changed files with 2980 additions and 4726 deletions

View file

@ -1,7 +1,4 @@
[api]
base_url = "https://api.vlw.one/"
api_key = ""
verify_peer = 0
[time]
date_time_zone = "Europe/Stockholm"
verify_peer = 0

8
.gitignore vendored
View file

@ -1,14 +1,8 @@
# Public assets #
#################
public/robots.txt
public/.well-known
assets/js/modules/npm
assets/media/content
# Bootstrapping #
#################
vendor
node_modules
.env.ini
# OS generated files #

View file

@ -1,38 +1,32 @@
# vlw.se
This is the source code behind [vlw.se](https://vlw.se) which has been written from the ground up by me. This website is built on top of my [Vegvisir web framework](https://vegvisir.vlw.se) and my [Reflect API framework](https://reflect.vlw.se).
This is the source code behind [vlw.se](https://vlw.se) which has been written from the ground up by me. This website is built on top of my [Vegvisir web framework](https://github.com/victorwesterlund/vegvisir) and my [Reflect API framework](https://github.com/victorwesterlund/reflect).
# Installation
If you for whatever reason want to get this website up and running for yourself this is how that is done.
## This website requires the following prerequisites
- [PHP 8.0+](https://www.php.net/)
- [MariaDB 14+](https://mariadb.org/)
- [The NPM package manager](https://www.npmjs.com/)
- [The Reflect API framework](https://reflect.vlw.se)
- [The Vegvisir web framework](https://vegvisir.vlw.se)
- [The composer package manager](https://getcomposer.org/)
This website is built for PHP 8.0+ and MariaDB 14+ (for the API database).
**Confimed supported framework versions:**
Vegvisir|Reflect
--|--
✅ [`3.1.0`](https://codeberg.org/vegvisir/vegvisir/releases/tag/3.1.0)|✅ [`2.7.2`](https://codeberg.org/reflect/reflect/releases/tag/2.7.2)
✅ [`2.3.0`](https://github.com/VictorWesterlund/vegvisir/releases/tag/2.3.0)|✅ [`2.6.2`](https://github.com/VictorWesterlund/reflect/releases/tag/2.6.2)
## Website (Vegvisir)
1. **Download this repo**
Git clone or download this repo to any local folder
```
git clone https://codeberg.org/vlw/vlw.se
git clone https://github.com/VictorWesterlund/vlw.se
```
2. **Download and install Vegvisir**
Follow the installation instructions for [Vegvisir](https://vegvisir.vlw.se/docs/installation) and point the `root_path` variable to your local vlw.se folder.
Follow the installation instructions for [Vegvisir](https://github.com/victorwesterlund/vegvisir) and point the `site_path` variable to the local vlw.se folder.
3. **Run the install script**
3. **Install dependencies**
This bash script will install dependencies and make npm modules public.
Install dependencies with composer.
```
./install.sh
composer install --optimize-autoloader
```
Et voila! You probably want to install the API-side too but the website itself should now be accessible from your configured Vegvisir host.
@ -46,16 +40,16 @@ The API (and database) is where most content is stored and served from on this w
Otherwise... Git clone or download this repo to any local folder
```
git clone https://codeberg.org/vlw/vlw.se
git clone https://github.com/VictorWesterlund/vlw.se
```
2. **Download and install Reflect**
Follow the installation instructions for [Reflect](https://reflect.vlw.se/docs/installation) and point the `endpoints` variable to the `/api` subdirectory in the local vlw.se folder.
Follow the installation instructions for [Reflect](https://github.com/victorwesterlund/vegvisir) and point the `endpoints` variable to the `/api` subdirectory in the local vlw.se folder.
3. **Install dependencies**
`cd` into the api folder and install dependencies with composer.
Install dependencies with composer.
```
composer install --optimize-autoloader
```
@ -68,6 +62,9 @@ The API (and database) is where most content is stored and served from on this w
Make a copy of `/api/.env.example.ini` and change the `[vlwdb]` variables with your MariaDB credentials.
You also have to generate a [GitHub access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) if you wish to use the `releases` endpoint.
[Read more about this endpoint here](#)
6. **Set environment variables for website**
It's reasonable to assume if you've installed the website from this repo that you'd also want to use the API with it. Start my making a copy of `/.env.example.ini` (root directory) and change the `[api]` variables to point to your API hostname.

View file

@ -1,11 +1,11 @@
[connect]
host = ""
user = ""
pass = ""
[vlwdb]
mariadb_host = ""
mariadb_user = ""
mariadb_pass = ""
mariadb_db = ""
[databases]
vlw = ""
battlestation = ""
[notes]
md_file_dir = ""
[github]
api_key = ""
# Use-Agent string sent to GitHub API
# They recommend setting it to your GitHub username or app name
user_agent = ""

View file

@ -1,8 +1,14 @@
{
"require": {
"local/api.endpoints": "1.0.0-dev",
"reflect/plugin-rules": "^1.5",
"victorwesterlund/xenum": "dev-master",
"vlw/mysql": "dev-master"
"victorwesterlund/xenum": "^1.1"
},
"minimum-stability": "dev"
"minimum-stability": "dev",
"repositories": [
{
"type": "path",
"url": "src/packages/Endpoints"
}
]
}

81
api/composer.lock generated
View file

@ -4,15 +4,46 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f3f2b3cb3bd789eee6af4a93f4a6e0f9",
"content-hash": "9da96ba90ef20d885034442b30dce0a3",
"packages": [
{
"name": "local/api.endpoints",
"version": "1.0.0-dev",
"dist": {
"type": "path",
"url": "src/packages/Endpoints",
"reference": "89b7b9a4cc504abddb4aeec8e05a95c9d9087575"
},
"type": "library",
"autoload": {
"psr-4": {
"VLW\\API\\": "src/"
}
},
"authors": [
{
"name": "Victor Westerlund",
"email": "victor@vlw.se"
}
],
"description": "Endpoint pathmappings for VLW API",
"transport-options": {
"relative": true
}
},
{
"name": "reflect/plugin-rules",
"version": "1.5.2",
"version": "1.5.0",
"source": {
"type": "git",
"url": "https://codeberg.org/reflect/reflect-rules-plugin",
"reference": "aa7d969350f50d00d7dce01b948276946fcc0e81"
"url": "https://github.com/VictorWesterlund/reflect-rules-plugin.git",
"reference": "9c837fd1944133edfed70a63ce8b32bf67f0d94b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/VictorWesterlund/reflect-rules-plugin/zipball/9c837fd1944133edfed70a63ce8b32bf67f0d94b",
"reference": "9c837fd1944133edfed70a63ce8b32bf67f0d94b",
"shasum": ""
},
"type": "library",
"autoload": {
@ -31,11 +62,15 @@
}
],
"description": "Add request search paramter and request body constraints to an API built with Reflect",
"time": "2024-11-28T17:05:16+00:00"
"support": {
"issues": "https://github.com/VictorWesterlund/reflect-rules-plugin/issues",
"source": "https://github.com/VictorWesterlund/reflect-rules-plugin/tree/1.5.0"
},
"time": "2024-01-17T11:07:44+00:00"
},
{
"name": "victorwesterlund/xenum",
"version": "dev-master",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/VictorWesterlund/php-xenum.git",
@ -47,7 +82,6 @@
"reference": "8972f06f42abd1f382807a67e937d5564bb89699",
"shasum": ""
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
@ -70,46 +104,17 @@
"source": "https://github.com/VictorWesterlund/php-xenum/tree/1.1.1"
},
"time": "2023-11-20T10:10:39+00:00"
},
{
"name": "vlw/mysql",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://codeberg.org/vlw/php-mysql",
"reference": "619f43b3bfab9eb034dca3e54c7466055240c861"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"vlw\\MySQL\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-3.0-or-later"
],
"authors": [
{
"name": "Victor Westerlund",
"email": "victor@vlw.se"
}
],
"description": "Abstraction library for common MySQL/MariaDB DML operations with php-mysqli",
"time": "2024-09-25T13:28:15+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
"victorwesterlund/xenum": 20,
"vlw/mysql": 20
"local/api.endpoints": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.0.0"
}

View file

@ -1,63 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Battlestation\Config\ConfigModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Battlestation/Config/Config.php");
class GET_Battlestation extends VLWdb {
protected Ruleset $ruleset;
private array $query;
private array $results = [];
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(ConfigModel::REF_MB_ID->value))
->type(Type::STRING)
->min(parent::UUID_LENGTH)
->max(parent::UUID_LENGTH),
(new Rules(ConfigModel::FRIENDLY_NAME->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
]);
parent::__construct(Databases::BATTLESTATION, $this->ruleset);
// Use a copy of search parameters
$this->query = $_GET;
}
private function get_config(): array {
return $this->results = $this->db
->for(ConfigModel::TABLE)
->where($this->query)
->order([ConfigModel::DATE_BUILT->value => "DESC"])
->select(ConfigModel::values())
->fetch_all(MYSQLI_ASSOC);
}
public function main(): Response {
// Set properties as "searchable"
parent::make_wildcard_search(ConfigModel::FRIENDLY_NAME->value, $this->query);
$this->get_config();
// Return 404 Not Found if response array is empty
return new Response($this->results, $this->results ? 200 : 404);
}
}

View file

@ -1,103 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Battlestation\ChassisModel;
use VLW\API\Databases\VLWdb\Models\Battlestation\Config\ChassisMbModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Battlestation/Chassis.php");
require_once Path::root("src/databases/models/Battlestation/Config/ChassisMb.php");
class GET_BattlestationChassis extends VLWdb {
private const REL_MOTHERBOARDS = "motherboards";
protected Ruleset $ruleset;
private array $query;
private array $results = [];
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(ChassisModel::ID->value))
->type(Type::STRING)
->min(parent::UUID_LENGTH)
->max(parent::UUID_LENGTH),
(new Rules(ChassisModel::VENDOR_NAME->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(ChassisModel::VENDOR_MODEL->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(ChassisModel::STORAGE_TWOINCHFIVE->value))
->type(Type::NUMBER)
->type(Type::NULL)
->min(0)
->max(parent::MYSQL_TINYINT_MAX_LENGTH),
(new Rules(ChassisModel::STORAGE_THREEINCHFIVE->value))
->type(Type::NUMBER)
->type(Type::NULL)
->min(0)
->max(parent::MYSQL_TINYINT_MAX_LENGTH),
(new Rules(ChassisModel::IS_RETIRED->value))
->type(Type::BOOLEAN)
]);
parent::__construct(Databases::BATTLESTATION, $this->ruleset);
// Use a copy of search parameters
$this->query = $_GET;
}
private function get_motherboards(): void {
foreach ($this->results as &$result) {
// Get motherboard id from relationship by chassis id
$result[self::REL_MOTHERBOARDS] = $this->db
->for(ChassisMbModel::TABLE)
->where([ChassisMbModel::REF_CHASSIS_ID->value => $result[ChassisModel::ID->value]])
->select(ChassisMbModel::values())
->fetch_all(MYSQLI_ASSOC);
}
}
private function get_chassis(): array {
return $this->results = $this->db
->for(ChassisModel::TABLE)
->where($this->query)
->order([ChassisModel::DATE_AQUIRED->value => "DESC"])
->select(ChassisModel::values())
->fetch_all(MYSQLI_ASSOC);
}
public function main(): Response {
// Set properties as "searchable"
parent::make_wildcard_search(ChassisModel::VENDOR_NAME->value, $this->query);
parent::make_wildcard_search(ChassisModel::VENDOR_MODEL->value, $this->query);
// Get hardware
$this->get_chassis();
// Resolve hardware relationships
$this->get_motherboards();
// Return 404 Not Found if response array is empty
return new Response($this->results, $this->results ? 200 : 404);
}
}

View file

@ -1,108 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Battlestation\{
CoolerModel
};
use VLW\API\Databases\VLWdb\Models\Battlestation\Config\MbCpuCoolerModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Battlestation/Coolers.php");
require_once Path::root("src/databases/models/Battlestation/Config/MbCpuCooler.php");
class GET_BattlestationCoolers extends VLWdb {
private const REL_MOTHERBOARDS = "motherboards";
protected Ruleset $ruleset;
private array $query;
private array $results = [];
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(CoolerModel::ID->value))
->type(Type::STRING)
->min(parent::UUID_LENGTH)
->max(parent::UUID_LENGTH),
(new Rules(CoolerModel::TYPE_LIQUID->value))
->type(Type::BOOLEAN),
(new Rules(CoolerModel::SIZE_FAN->value))
->type(Type::NULL)
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INTR_MAX_LENGTH),
(new Rules(CoolerModel::SIZE_RADIATOR->value))
->type(Type::NULL)
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INTR_MAX_LENGTH),
(new Rules(CoolerModel::VENDOR_NAME->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(CoolerModel::VENDOR_MODEL->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(CoolerModel::IS_RETIRED->value))
->type(Type::BOOLEAN)
]);
parent::__construct(Databases::BATTLESTATION, $this->ruleset);
// Use a copy of search parameters
$this->query = $_GET;
}
private function get_motherboards(): void {
foreach ($this->results as &$result) {
// Get motherboard id from relationship by chassis id
$result[self::REL_MOTHERBOARDS] = $this->db
->for(MbCoolerModel::TABLE)
->where([MbCoolerModel::REF_COOLER_ID->value => $result[CoolerModel::ID->value]])
->select(MbCoolerModel::values())
->fetch_all(MYSQLI_ASSOC);
}
}
private function get_coolers(): array {
return $this->results = $this->db
->for(CoolerModel::TABLE)
->where($this->query)
->order([CoolerModel::DATE_AQUIRED->value => "DESC"])
->select(CoolerModel::values())
->fetch_all(MYSQLI_ASSOC);
}
public function main(): Response {
// Set properties as "searchable"
parent::make_wildcard_search(CoolerModel::VENDOR_NAME->value, $this->query);
parent::make_wildcard_search(CoolerModel::VENDOR_MODEL->value, $this->query);
// Get hardware
$this->get_coolers();
// Resolve hardware relationships
$this->get_motherboards();
// Return 404 Not Found if response array is empty
return new Response($this->results, $this->results ? 200 : 404);
}
}

View file

@ -1,117 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Battlestation\{
CpuModel,
ClassEnum
};
use VLW\API\Databases\VLWdb\Models\Battlestation\Config\MbCpuCoolerModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Battlestation/Cpu.php");
require_once Path::root("src/databases/models/Battlestation/Config/MbCpuCooler.php");
class GET_BattlestationCpu extends VLWdb {
private const REL_MOTHERBOARDS = "motherboards";
protected Ruleset $ruleset;
private array $query;
private array $results = [];
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(CpuModel::ID->value))
->type(Type::STRING)
->min(parent::UUID_LENGTH)
->max(parent::UUID_LENGTH),
(new Rules(CpuModel::CLOCK_BASE->value))
->type(Type::NUMBER)
->min(1),
(new Rules(CpuModel::CLOCK_TURBO->value))
->type(Type::NUMBER)
->min(1),
(new Rules(CpuModel::CORE_COUNT_PERFORMANCE->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_TINYINT_MAX_LENGTH),
(new Rules(CpuModel::CORE_COUNT_EFFICIENCY->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_TINYINT_MAX_LENGTH),
(new Rules(CpuModel::CORE_THREADS->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_TINYINT_MAX_LENGTH),
(new Rules(CpuModel::VENDOR_NAME->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(CpuModel::VENDOR_MODEL->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(CpuModel::IS_RETIRED->value))
->type(Type::BOOLEAN)
]);
parent::__construct(Databases::BATTLESTATION, $this->ruleset);
// Use a copy of search parameters
$this->query = $_GET;
}
private function get_motherboards(): void {
foreach ($this->results as &$result) {
// Get motherboard id from relationship by chassis id
$result[self::REL_MOTHERBOARDS] = $this->db
->for(MbCpuCoolerModel::TABLE)
->where([MbCpuCoolerModel::REF_CPU_ID->value => $result[CpuModel::ID->value]])
->select(MbCpuCoolerModel::values())
->fetch_all(MYSQLI_ASSOC);
}
}
private function get_cpu(): array {
return $this->results = $this->db
->for(CpuModel::TABLE)
->where($this->query)
->order([CpuModel::DATE_AQUIRED->value => "DESC"])
->select(CpuModel::values())
->fetch_all(MYSQLI_ASSOC);
}
public function main(): Response {
// Set properties as "searchable"
parent::make_wildcard_search(CpuModel::VENDOR_NAME->value, $this->query);
parent::make_wildcard_search(CpuModel::VENDOR_MODEL->value, $this->query);
// Get hardware
$this->get_cpu();
// Resolve hardware relationships
$this->get_motherboards();
// Return 404 Not Found if response array is empty
return new Response($this->results, $this->results ? 200 : 404);
}
}

View file

@ -1,115 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Battlestation\{
DramModel,
DramFormfactorEnum,
DramTechnologyEnum
};
use VLW\API\Databases\VLWdb\Models\Battlestation\Config\MbDramModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Battlestation/Dram.php");
require_once Path::root("src/databases/models/Battlestation/Config/MbDram.php");
class GET_BattlestationDram extends VLWdb {
private const REL_MOTHERBOARDS = "motherboards";
protected Ruleset $ruleset;
private array $query;
private array $results = [];
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(DramModel::ID->value))
->type(Type::STRING)
->min(parent::UUID_LENGTH)
->max(parent::UUID_LENGTH),
(new Rules(DramModel::CAPACITY->value))
->type(Type::NUMBER)
->min(1),
(new Rules(DramModel::SPEED->value))
->type(Type::NUMBER)
->min(1),
(new Rules(DramModel::FORMFACTOR->value))
->type(Type::ENUM, DramFormfactorEnum::names()),
(new Rules(DramModel::TECHNOLOGY->value))
->type(Type::ENUM, DramTechnologyEnum::names()),
(new Rules(DramModel::ECC->value))
->type(Type::BOOLEAN),
(new Rules(DramModel::BUFFERED->value))
->type(Type::BOOLEAN),
(new Rules(DramModel::VENDOR_NAME->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(DramModel::VENDOR_MODEL->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(DramModel::IS_RETIRED->value))
->type(Type::BOOLEAN)
]);
parent::__construct(Databases::BATTLESTATION, $this->ruleset);
// Use a copy of search parameters
$this->query = $_GET;
}
private function get_motherboards(): void {
foreach ($this->results as &$result) {
// Get motherboard id from relationship by chassis id
$result[self::REL_MOTHERBOARDS] = $this->db
->for(MbDramModel::TABLE)
->where([MbDramModel::REF_DRAM_ID->value => $result[DramModel::ID->value]])
->select(MbDramModel::values())
->fetch_all(MYSQLI_ASSOC);
}
}
private function get_dram(): array {
return $this->results = $this->db
->for(DramModel::TABLE)
->where($this->query)
->order([DramModel::DATE_AQUIRED->value => "DESC"])
->select(DramModel::values())
->fetch_all(MYSQLI_ASSOC);
}
public function main(): Response {
// Set properties as "searchable"
parent::make_wildcard_search(DramModel::VENDOR_NAME->value, $this->query);
parent::make_wildcard_search(DramModel::VENDOR_MODEL->value, $this->query);
// Get hardware
$this->get_dram();
// Resolve hardware relationships
$this->get_motherboards();
// Return 404 Not Found if response array is empty
return new Response($this->results, $this->results ? 200 : 404);
}
}

View file

@ -1,107 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Battlestation\GpuModel;
use VLW\API\Databases\VLWdb\Models\Battlestation\Config\MbGpuModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Battlestation/Gpu.php");
require_once Path::root("src/databases/models/Battlestation/Config/MbGpu.php");
class GET_BattlestationGpu extends VLWdb {
private const REL_MOTHERBOARDS = "motherboards";
protected Ruleset $ruleset;
private array $query;
private array $results = [];
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(GpuModel::ID->value))
->type(Type::STRING)
->min(parent::UUID_LENGTH)
->max(parent::UUID_LENGTH),
(new Rules(GpuModel::MEMORY->value))
->type(Type::NUMBER)
->min(1),
(new Rules(GpuModel::VENDOR_NAME->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(GpuModel::VENDOR_MODEL->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(GpuModel::VENDOR_CHIP_NAME->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(GpuModel::VENDOR_CHIP_MODEL->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(GpuModel::IS_RETIRED->value))
->type(Type::BOOLEAN)
]);
parent::__construct(Databases::BATTLESTATION, $this->ruleset);
// Use a copy of search parameters
$this->query = $_GET;
}
private function get_motherboards(): void {
foreach ($this->results as &$result) {
// Get motherboard id from relationship by chassis id
$result[self::REL_MOTHERBOARDS] = $this->db
->for(MbGpuModel::TABLE)
->where([MbGpuModel::REF_GPU_ID->value => $result[GpuModel::ID->value]])
->select(MbGpuModel::values())
->fetch_all(MYSQLI_ASSOC);
}
}
private function get_gpu(): array {
return $this->results = $this->db
->for(GpuModel::TABLE)
->where($this->query)
->order([GpuModel::DATE_AQUIRED->value => "DESC"])
->select(GpuModel::values())
->fetch_all(MYSQLI_ASSOC);
}
public function main(): Response {
// Set properties as "searchable"
parent::make_wildcard_search(GpuModel::VENDOR_NAME->value, $this->query);
parent::make_wildcard_search(GpuModel::VENDOR_MODEL->value, $this->query);
parent::make_wildcard_search(GpuModel::VENDOR_CHIP_NAME->value, $this->query);
parent::make_wildcard_search(GpuModel::VENDOR_CHIP_MODEL->value, $this->query);
// Get hardware
$this->get_gpu();
// Resolve hardware relationships
$this->get_motherboards();
// Return 404 Not Found if response array is empty
return new Response($this->results, $this->results ? 200 : 404);
}
}

View file

@ -1,194 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Battlestation\{
MbModel,
MbFormfactorEnum
};
use VLW\API\Databases\VLWdb\Models\Battlestation\Config\{
MbGpuModel,
MbPsuModel,
MbDramModel,
MbStorageModel,
ChassisMbModel,
MbCpuCoolerModel
};
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Battlestation/Mb.php");
require_once Path::root("src/databases/models/Battlestation/Config/MbPsu.php");
require_once Path::root("src/databases/models/Battlestation/Config/MbGpu.php");
require_once Path::root("src/databases/models/Battlestation/Config/MbDram.php");
require_once Path::root("src/databases/models/Battlestation/Config/MbStorage.php");
require_once Path::root("src/databases/models/Battlestation/Config/ChassisMb.php");
require_once Path::root("src/databases/models/Battlestation/Config/MbCpuCooler.php");
class GET_BattlestationMb extends VLWdb {
private const REL_CPU = "cpus";
private const REL_PSU = "psus";
private const REL_GPU = "gpus";
private const REL_DRAM = "dram";
private const REL_STORAGE = "storage";
private const REL_CHASSIS = "chassis";
protected Ruleset $ruleset;
private array $query;
private array $results = [];
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(MbModel::ID->value))
->type(Type::STRING)
->min(parent::UUID_LENGTH)
->max(parent::UUID_LENGTH),
(new Rules(MbModel::FORMFACTOR->value))
->type(Type::ENUM, MbFormfactorEnum::names()),
(new Rules(MbModel::VENDOR_NAME->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(MbModel::VENDOR_MODEL->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(MbModel::NETWORK_ETHERNET->value))
->type(Type::NULL)
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(MbModel::NETWORK_WLAN->value))
->type(Type::NULL)
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(MbModel::NETWORK_BLUETOOTH->value))
->type(Type::NULL)
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(MbModel::IS_RETIRED->value))
->type(Type::BOOLEAN)
]);
parent::__construct(Databases::BATTLESTATION, $this->ruleset);
// Use a copy of search parameters
$this->query = $_GET;
}
private function get_chassis(): void {
foreach ($this->results as &$result) {
// Get motherboard id from relationship by chassis id
$result[self::REL_CHASSIS] = $this->db
->for(ChassisMbModel::TABLE)
->where([ChassisMbModel::REF_MB_ID->value => $result[MbModel::ID->value]])
->select(ChassisMbModel::values())
->fetch_all(MYSQLI_ASSOC);
}
}
private function get_psu(): void {
foreach ($this->results as &$result) {
// Get motherboard id from relationship by chassis id
$result[self::REL_PSU] = $this->db
->for(MbPsuModel::TABLE)
->where([MbPsuModel::REF_MB_ID->value => $result[MbModel::ID->value]])
->select(MbPsuModel::values())
->fetch_all(MYSQLI_ASSOC);
}
}
private function get_cpu(): void {
foreach ($this->results as &$result) {
// Get motherboard id from relationship by chassis id
$result[self::REL_CPU] = $this->db
->for(MbCpuCoolerModel::TABLE)
->where([MbCpuCoolerModel::REF_MB_ID->value => $result[MbModel::ID->value]])
->select(MbCpuCoolerModel::values())
->fetch_all(MYSQLI_ASSOC);
}
}
private function get_gpu(): void {
foreach ($this->results as &$result) {
// Get motherboard id from relationship by chassis id
$result[self::REL_GPU] = $this->db
->for(MbGpuModel::TABLE)
->where([MbGpuModel::REF_MB_ID->value => $result[MbModel::ID->value]])
->select(MbGpuModel::values())
->fetch_all(MYSQLI_ASSOC);
}
}
private function get_dram(): void {
foreach ($this->results as &$result) {
// Get motherboard id from relationship by chassis id
$result[self::REL_DRAM] = $this->db
->for(MbDramModel::TABLE)
->where([MbDramModel::REF_MB_ID->value => $result[MbModel::ID->value]])
->select(MbDramModel::values())
->fetch_all(MYSQLI_ASSOC);
}
}
private function get_storage(): void {
foreach ($this->results as &$result) {
// Get motherboard id from relationship by chassis id
$result[self::REL_STORAGE] = $this->db
->for(MbStorageModel::TABLE)
->where([MbStorageModel::REF_MB_ID->value => $result[MbModel::ID->value]])
->select(MbStorageModel::values())
->fetch_all(MYSQLI_ASSOC);
}
}
// ----
private function get_motherboards(): array {
return $this->results = $this->db
->for(MbModel::TABLE)
->where($this->query)
->order([MbModel::DATE_AQUIRED->value => "DESC"])
->select(MbModel::values())
->fetch_all(MYSQLI_ASSOC);
}
public function main(): Response {
// Set properties as "searchable"
parent::make_wildcard_search(MbModel::VENDOR_NAME->value, $this->query);
parent::make_wildcard_search(MbModel::VENDOR_MODEL->value, $this->query);
// Get hardware
$this->get_motherboards();
// Resolve hardware relationships
$this->get_chassis();
$this->get_cpu();
$this->get_psu();
$this->get_gpu();
$this->get_dram();
$this->get_storage();
// Return 404 Not Found if response array is empty
return new Response($this->results, $this->results ? 200 : 404);
}
}

View file

@ -1,102 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Battlestation\{
PsuModel,
EightyplusRatingEnum
};
use VLW\API\Databases\VLWdb\Models\Battlestation\Config\MbPsuModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Battlestation/Psu.php");
require_once Path::root("src/databases/models/Battlestation/Config/MbPsu.php");
class GET_BattlestationPsu extends VLWdb {
private const REL_MOTHERBOARDS = "motherboards";
protected Ruleset $ruleset;
private array $query;
private array $results = [];
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(PsuModel::ID->value))
->type(Type::STRING)
->min(parent::UUID_LENGTH)
->max(parent::UUID_LENGTH),
(new Rules(PsuModel::POWER->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH),
(new Rules(PsuModel::EIGHTYPLUS_RATING->value))
->type(Type::ENUM, EightyplusRatingEnum::names()),
(new Rules(PsuModel::VENDOR_NAME->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(PsuModel::VENDOR_MODEL->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(PsuModel::IS_RETIRED->value))
->type(Type::BOOLEAN)
]);
parent::__construct(Databases::BATTLESTATION, $this->ruleset);
// Use a copy of search parameters
$this->query = $_GET;
}
private function get_motherboards(): void {
foreach ($this->results as &$result) {
// Get motherboard id from relationship by chassis id
$result[self::REL_MOTHERBOARDS] = $this->db
->for(MbPsuModel::TABLE)
->where([MbPsuModel::REF_PSU_ID->value => $result[PsuModel::ID->value]])
->select(MbPsuModel::values())
->fetch_all(MYSQLI_ASSOC);
}
}
private function get_psu(): array {
return $this->results = $this->db
->for(PsuModel::TABLE)
->where($this->query)
->order([PsuModel::DATE_AQUIRED->value => "DESC"])
->select(PsuModel::values())
->fetch_all(MYSQLI_ASSOC);
}
public function main(): Response {
// Set properties as "searchable"
parent::make_wildcard_search(PsuModel::VENDOR_NAME->value, $this->query);
parent::make_wildcard_search(PsuModel::VENDOR_MODEL->value, $this->query);
// Get hardware
$this->get_psu();
// Resolve hardware relationships
$this->get_motherboards();
// Return 404 Not Found if response array is empty
return new Response($this->results, $this->results ? 200 : 404);
}
}

View file

@ -1,110 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Battlestation\{
StorageModel,
StorageDiskTypeEnum,
StorageDiskInterfaceEnum,
StorageDiskFormfactorEnum
};
use VLW\API\Databases\VLWdb\Models\Battlestation\Config\MbStorageModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Battlestation/Storage.php");
require_once Path::root("src/databases/models/Battlestation/Config/MbStorage.php");
class GET_BattlestationStorage extends VLWdb {
private const REL_MOTHERBOARDS = "motherboards";
protected Ruleset $ruleset;
private array $query;
private array $results = [];
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(StorageModel::ID->value))
->type(Type::STRING)
->min(parent::UUID_LENGTH)
->max(parent::UUID_LENGTH),
(new Rules(StorageModel::DISK_TYPE->value))
->type(Type::ENUM, StorageDiskTypeEnum::names()),
(new Rules(StorageModel::DISK_SIZE->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH),
(new Rules(StorageModel::DISK_INTERFACE->value))
->type(Type::ENUM, StorageDiskInterfaceEnum::names()),
(new Rules(StorageModel::DISK_FORMFACTOR->value))
->type(Type::ENUM, StorageDiskFormfactorEnum::names()),
(new Rules(StorageModel::VENDOR_NAME->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(StorageModel::VENDOR_MODEL->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(StorageModel::IS_RETIRED->value))
->type(Type::BOOLEAN)
]);
parent::__construct(Databases::BATTLESTATION, $this->ruleset);
// Use a copy of search parameters
$this->query = $_GET;
}
private function get_motherboards(): void {
foreach ($this->results as &$result) {
// Get motherboard id from relationship by chassis id
$result[self::REL_MOTHERBOARDS] = $this->db
->for(MbStorageModel::TABLE)
->where([MbStorageModel::REF_STORAGE_ID->value => $result[StorageModel::ID->value]])
->select(MbStorageModel::values())
->fetch_all(MYSQLI_ASSOC);
}
}
private function get_storage(): array {
return $this->results = $this->db
->for(StorageModel::TABLE)
->where($this->query)
->order([StorageModel::DATE_AQUIRED->value => "DESC"])
->select(StorageModel::values())
->fetch_all(MYSQLI_ASSOC);
}
public function main(): Response {
// Set properties as "searchable"
parent::make_wildcard_search(StorageModel::VENDOR_NAME->value, $this->query);
parent::make_wildcard_search(StorageModel::VENDOR_MODEL->value, $this->query);
// Get hardware
$this->get_storage();
// Resolve hardware relationships
$this->get_motherboards();
// Return 404 Not Found if response array is empty
return new Response($this->results, $this->results ? 200 : 404);
}
}

39
api/endpoints/coffee/GET.php Executable file
View file

@ -0,0 +1,39 @@
<?php
use Reflect\Path;
use Reflect\Response;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Coffee\CoffeeModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Coffee.php");
class GET_Coffee extends VLWdb {
const LIST_LIMIT = 20;
public function __construct() {
parent::__construct();
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
public function main(): Response {
// Get the last LIST_LIMIT coffees from the database
$resp = $this->db->for(CoffeeModel::TABLE)
->order([CoffeeModel::DATE_TIMESTAMP_CREATED->value => "DESC"])
->limit(self::LIST_LIMIT)
->select([
CoffeeModel::ID->value,
CoffeeModel::DATE_TIMESTAMP_CREATED->value
]);
return parent::is_mysqli_result($resp)
? new Response($resp->fetch_all(MYSQLI_ASSOC))
: $this->resp_database_error();
}
}

36
api/endpoints/coffee/POST.php Executable file
View file

@ -0,0 +1,36 @@
<?php
use Reflect\Path;
use Reflect\Response;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Coffee\CoffeeModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Coffee.php");
class POST_Coffee extends VLWdb {
public function __construct() {
parent::__construct();
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to record coffee! Ugh please take a note somewhere else", 503);
}
public function main(): Response {
// Generate UUID for entity
$id = parent::gen_uuid4();
// Attempt to create new entity
$insert = $this->db->for(CoffeeModel::TABLE)
->insert([
CoffeeModel::ID->value => $id,
CoffeeModel::DATE_TIMESTAMP_CREATED->value => time(),
]);
// Return 201 Created and entity id if successful
return $insert ? new Response($id, 201) : $this->resp_database_error();
}
}

95
api/endpoints/media/GET.php Executable file
View file

@ -0,0 +1,95 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Media\MediaModel;
use victorwesterlund\xEnum;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Media.php");
enum MediaDispositionEnum: string {
use xEnum;
case METADATA = "metadata";
case INLINE = "inline";
case DOWNLOAD = "download";
}
class GET_Media extends VLWdb {
const GET_DISPOSITION_KEY = "disposition";
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(MediaModel::ID->value))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(self::GET_DISPOSITION_KEY))
->type(Type::ENUM, MediaDispositionEnum::values())
->default(MediaDispositionEnum::METADATA->value)
]);
}
// # Helper methods
private function fetch_srcset(string $id): array {
$resp = $this->db->for(WorkTagsModel::TABLE)
->where([WorkTagsModel::ANCHOR->value => $id])
->select(WorkTagsModel::NAME->value);
return parent::is_mysqli_result($resp) ? $resp->fetch_all(MYSQLI_ASSOC) : [];
}
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
public function main(): Response {
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
$resp = $this->db->for(MediaModel::TABLE)
->where([MediaModel::ID->value => $_GET[MediaModel::ID->value]])
->select([
MediaModel::ID->value,
MediaModel::NAME->value,
MediaModel::TYPE->value,
MediaModel::MIME->value,
MediaModel::EXTENSION->value,
MediaModel::SRCSET->value,
MediaModel::DATE_TIMESTAMP_CREATED->value,
]);
// Bail out if something went wrong retrieving rows from the database
if (!parent::is_mysqli_result($resp)) {
return $this->resp_database_error();
}
$media = $resp->fetch_assoc();
$test = true;
}
}

117
api/endpoints/media/POST.php Executable file
View file

@ -0,0 +1,117 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Media\MediaModel;
use VLW\API\Databases\VLWdb\Models\Media\MediaTypeEnum;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Media.php");
class POST_Media extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(MediaModel::ID->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
->default(parent::gen_uuid4()),
(new Rules(MediaModel::NAME->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
->default(null),
(new Rules(MediaModel::TYPE->value))
->type(Type::ENUM, MediaTypeEnum::values())
->default(null),
(new Rules(MediaModel::EXTENSION->value))
->type(Type::STRING)
->min(3)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
->default(null),
(new Rules(MediaModel::MIME->value))
->type(Type::STRING)
->min(3)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
->default(null),
(new Rules(MediaModel::SRCSET->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
->default(null)
]);
}
// # Helper methods
// Returns true if an srcset exists for provided key
private static function media_srcset_exists(): bool {
// No srcet get parameter has been set
if (empty($_POST[MediaModel::SRCSET->value])) {
return true;
}
// Check if the provided srcset exists by calling the srcset endpoint
return Call("media/srcset?id={$_POST[MediaModel::SRCSET->value]}", Method::GET)->ok;
}
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
public function main(): Response {
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
// Bail out if an srcset doesn't exist
if (!self::media_srcset_exists()) {
return new Response("No media srcset exists with id '{$_POST[MediaModel::SRCSET->value]}'", 404);
}
$insert = $this->db->for(MediaModel::TABLE)
->insert([
MediaModel::ID->value => $_POST[MediaModel::ID->value],
MediaModel::NAME->value => $_POST[MediaModel::NAME->value],
MediaModel::MIME->value => $_POST[MediaModel::MIME->value],
// Strip dots from extension string if set
MediaModel::EXTENSION->value => $_POST[MediaModel::EXTENSION->value]
? str_replace(".", "", $_POST[MediaModel::EXTENSION->value])
: null,
MediaModel::SRCSET->value => $_POST[MediaModel::SRCSET->value],
MediaModel::DATE_TIMESTAMP_CREATED->value => time()
]);
// Return media id if insert was successful
return $insert
? new Response($_POST[MediaModel::ID->value], 201)
: $this->resp_database_error();
}
}

View file

@ -0,0 +1,106 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Media\MediaModel;
use VLW\API\Databases\VLWdb\Models\MediaSrcset\MediaSrcsetModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Media.php");
require_once Path::root("src/databases/models/MediaSrcset.php");
class GET_MediaSrcset extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(MediaSrcsetModel::ID->value))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
]);
}
// # Helper methods
// Get metadata for the requested srcset
private function get_srcset(): array|false {
$srcset = $this->db->for(MediaSrcsetModel::TABLE)
->where([MediaSrcsetModel::ID->value => $_GET[MediaSrcsetModel::ID->value]])
->select([MediaSrcsetModel::ANCHOR_DEFAULT->value]);
// Something went wrong retrieving rows from the database
if (!parent::is_mysqli_result($srcset)) {
return false;
}
// Return assoc array of srcset data if it exists
return $srcset->num_rows === 1 ? $srcset->fetch_assoc() : false;
}
// Get all media entities that are part of the requested srcset
private function get_srcset_media(): mysqli_result|false {
$media = $this->db->for(MediaModel::TABLE)
->where([MediaModel::SRCSET->value => $_GET[MediaSrcsetModel::ID->value]])
->select([
MediaModel::ID->value,
MediaModel::TYPE->value,
MediaModel::MIME->value,
MediaModel::EXTENSION->value
]);
return parent::is_mysqli_result($media) ? $media : false;
}
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
public function main(): Response {
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
// Get srcset data
$srcset = $this->get_srcset();
if (!$srcset) {
return new Response("No media srcset exist with id '{$_GET[MediaSrcsetModel::ID->value]}'", 404);
}
$media = $this->get_srcset_media();
if (!$media) {
return new Response("Failed to fetch srcset media", 500);
}
$media_entities = $media->fetch_all(MYSQLI_ASSOC);
// This is the id of the media entity that is considered the default or "fallback"
$srcet_default_media_id = $srcset[MediaSrcsetModel::ANCHOR_DEFAULT->value];
// Return assoc array of all media entities that are in this srcset
return new Response([
// Return default media entity separately from the rest of the srcset as an assoc array
"default" => array_filter($media_entities, fn(array $entity) => $entity[MediaModel::ID->value] === $srcet_default_media_id)[0],
// Return all media that isn't default as array of assoc arrays
"srcset" => array_filter($media_entities, fn(array $entity) => $entity[MediaModel::ID->value] !== $srcet_default_media_id)
]);
}
}

View file

@ -0,0 +1,55 @@
<?php
use Reflect\Path;
use Reflect\Response;
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\MediaSrcset\MediaSrcsetModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Media.php");
require_once Path::root("src/databases/models/MediaSrcset.php");
class POST_MediaSrcset extends VLWdb {
public function __construct() {
parent::__construct();
}
// # Responses
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
public function main(): Response {
// Generate a random UUID for this srcset
$id = parent::gen_uuid4();
// Ensure an srcset with the generated id doesn't exist, although it shouldn't realistically ever happen
$srcset_existing = Call("media/srcset?id={$id}", Method::GET);
if ($srcset_existing->code !== 404) {
// Wow a UUID4 collision... buy a lottery ticket
if ($srcset_existing->code === 200) {
return $this->main();
}
// Failed to get srcset
return new Response("Something went wrong when checking if the srcset exists", 500);
}
// Create new srcset entity
$insert = $this->db->for(MediaSrcsetModel::TABLE)
->insert([
MediaSrcsetModel::ID->value => $id
]);
// Return created srcset id if successful
return $insert
? new Response($id, 201)
: $this->resp_database_error();
}
}

View file

@ -6,19 +6,20 @@
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Messages\MessagesModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Messages/Messages.php");
require_once Path::root("src/databases/models/Messages.php");
class POST_Messages extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
@ -33,19 +34,46 @@
->min(1)
->max(parent::MYSQL_TEXT_MAX_LENGTH)
]);
}
parent::__construct(Databases::VLW, $this->ruleset);
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
public function main(): Response {
// Use copy of request body as entity
$entity = $_POST;
//return new Response(["hello" => "maybe"], 500);
$entity[MessagesModel::ID->value] = parent::gen_uuid4();
$entity[MessagesModel::DATE_CREATED->value] = time();
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
return $this->db->for(MessagesModel::TABLE)->insert($entity) === true
? new Response($entity[MessagesModel::ID->value], 201)
: new Response("Failed to create message", 500);
// Generate UUID for entity
$id = parent::gen_uuid4();
// Attempt to create new entity
$insert = $this->db->for(MessagesModel::TABLE)
->insert([
MessagesModel::ID->value => $id,
MessagesModel::EMAIL->value => $_POST["email"],
MessagesModel::MESSAGE->value => $_POST["message"],
MessagesModel::DATE_TIMESTAMP_CREATED->value => time(),
]);
// Bail out if insert failed
if (!$insert) {
return $this->resp_database_error();
}
// Return 201 Created and entity id
return new Response($id, 201);
}
}

View file

@ -1,46 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
const GET_PARAM_NOTE = "id";
class GET_Notes {
protected readonly Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(GET_PARAM_NOTE))
->required()
->type(Type::STRING)
->min(1)
]);
$this->ruleset->validate_or_exit();
}
private function get_path(): string {
$base_path = substr($_ENV["notes"]["md_file_dir"], -1, 1) === "/" ? $_ENV["notes"]["md_file_dir"] : $_ENV["notes"]["md_file_dir"] . "/";
return "{$base_path}{$_GET[GET_PARAM_NOTE]}.md";
}
public function exists(): bool {
return is_readable($this->get_path());
}
public function read(): string {
return file_get_contents($this->get_path());
}
public function main(): Response {
return $this->exists()
? new Response($this->read(), 200, "text/plain")
: new Response("Note not found", 404);
}
}

223
api/endpoints/releases/POST.php Executable file
View file

@ -0,0 +1,223 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsModel;
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsNameEnum;
use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel;
require_once Path::root("src/databases/models/Work.php");
require_once Path::root("src/databases/models/WorkTags.php");
require_once Path::root("src/databases/models/WorkActions.php");
// "Virtual" database model for the POST request body since we're not writing to a db directly
enum ReleasesPostModel: string {
case GITHUB_USER = "user";
case GITHUB_REPO = "repo";
case GITHUB_TAG = "tag";
}
class POST_Releases {
// Base URL of the GitHub API (no tailing slash)
const GITHUB_API = "https://api.github.com";
const REGEX_HANDLE = "/@[\w]+/";
const REGEX_URL = "/\b(?:https?):\/\/\S+\b/";
protected Ruleset $ruleset;
protected CurlHandle $curl;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(ReleasesPostModel::GITHUB_USER->value))
->required()
->type(Type::STRING)
->min(1),
(new Rules(ReleasesPostModel::GITHUB_REPO->value))
->required()
->type(Type::STRING)
->min(1),
(new Rules(ReleasesPostModel::GITHUB_TAG->value))
->required()
->type(Type::STRING)
->type(Type::NUMBER)
->min(1)
]);
$this->curl = curl_init();
curl_setopt($this->curl, CURLOPT_USERAGENT, $_ENV["github"]["user_agent"]);
curl_setopt($this->curl, CURLOPT_HEADER, true);
curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->curl, CURLOPT_HTTPHEADER, [
"Accept" => "application/vnd.github+json",
"Authorization" => "token {$_ENV["github"]["api_key"]}",
"X-GitHub-Api-Version" => "2022-11-28"
]);
}
// # GitHub
// Generate HTML from a GitHub "auto-generate" release body
protected static function gh_auto_release_md_to_html(string $md): string {
$output = "";
// Parse each line of markdown
$lines = explode(PHP_EOL, $md);
foreach ($lines as $i => $line) {
// Ignore header line from releases
if ($i < 1) continue;
// Replace all URLs with HTMLAnchor tags, they will be PRs
$links = [];
preg_match_all(self::REGEX_URL, $line, $links, PREG_UNMATCHED_AS_NULL);
foreach ($links as $i => $link) {
if (empty($link)) continue;
// Last crumb from link pathname will be the PR id
$pr_id = explode("/", $link[$i]);
$pr_id = end($pr_id);
$line = str_replace($link, "<a href='{$link[$i]}'>{$pr_id}</a>", $line);
}
// Replace all at-handles with links to GitHub user profiles
$handles = [];
preg_match_all(self::REGEX_HANDLE, $line, $handles, PREG_UNMATCHED_AS_NULL);
foreach ($handles as $i => $handle) {
if (empty($handle)) continue;
// GitHub user URL without the "@"
$url = "https://github.com/" . substr($handle[$i], 1);
$line = str_replace($handle, "<a href='{$url}'>{$handle[$i]}</a>", $line);
}
$output .= "<p>{$line}</p>";
}
return $output;
}
// Return fully qualified URL to GitHub API releases endpoint
private static function get_url(): string {
return implode("/", [
self::GITHUB_API,
"repos",
$_POST[ReleasesPostModel::GITHUB_USER->value],
$_POST[ReleasesPostModel::GITHUB_REPO->value],
"releases",
"tags",
$_POST[ReleasesPostModel::GITHUB_TAG->value],
]);
}
// Fetch release information from GitHub API
private function fetch_release_data(): array {
$url = self::get_url();
curl_setopt($this->curl, CURLOPT_URL, self::get_url());
$resp = curl_exec($this->curl);
$header_size = curl_getinfo($this->curl, CURLINFO_HEADER_SIZE);
$header = substr($resp, 0, $header_size);
$body = substr($resp, $header_size);
return json_decode($body, true);
}
// # Sup
private function create_link_to_release_page(string $id, string $href): Response {
return Call("work/actions?id={$id}", Method::POST, [
WorkActionsModel::DISPLAY_TEXT->value => "Release details",
WorkActionsModel::HREF->value => $href,
WorkActionsModel::EXTERNAL->value => true
]);
}
// Create a tag for entity
private function create_tag(string $id, WorkTagsNameEnum $tag): Response {
return Call("work/tags?id={$id}", Method::POST, [
// Set "RELEASE" tag on new entity
WorkTagsModel::NAME->value => $tag->value
]);
}
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
public function main(): Response {
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
$data = $this->fetch_release_data();
if (!$data) {
return new Response("Failed to fetch release data", 500);
}
// Transform repo name to lowercase for summary title
$title = strtolower($_POST["repo"]);
// Use repo name and tag name as heading for summary
$summary = "<h3>Release {$title}@{$data["name"]}</h3>";
// Append HTML-ified release notes from GitHub to summary
$summary .= self::gh_auto_release_md_to_html($data["body"]);
$date_published = new \DateTime($data["published_at"], new \DateTimeZone("UTC"));
// Create work entity
$work_entity = Call("work", Method::POST, [
WorkModel::SUMMARY->value => $summary,
// Convert time created to Unix timestamp for work endpoint
WorkModel::DATE_TIMESTAMP_CREATED->value => $date_published->format("U"),
]);
// Bail out if creating the work entity failed
if (!$work_entity->ok) {
return new Response("Failed to create work entity for release", 500);
}
$work_entity_id = $work_entity->output();
// Create entity tags for release
$tags = [
WorkTagsNameEnum::VLW,
WorkTagsNameEnum::RELEASE
];
foreach ($tags as $tag) {
// Create entity tag for release or exit if failed to create
if (!$this->create_tag($work_entity_id, $tag)->ok) {
return new Response("Failed to create {$tag->name} tag for release entity", 500);
}
}
// Create link to release page on GitHub
if (!$this->create_link_to_release_page($work_entity_id, $data["html_url"])) {
return new Response("Failed to create link to release page on GitHub", 500);
}
return new Response($work_entity_id, 201);
}
}

View file

@ -9,15 +9,13 @@
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel;
require_once Path::root("src/Endpoints.php");
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/Work.php");
require_once Path::root("src/databases/models/Work.php");
require_once Path::root("src/databases/models/WorkActions.php");
class GET_Search extends VLWdb {
const GET_QUERY = "q";
@ -25,35 +23,118 @@
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(self::GET_QUERY))
->required()
->type(Type::STRING)
->min(1)
->min(2)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
private function search_work(): Response {
return (new Call(Endpoints::WORK->value))->params([
WorkModel::TITLE->value => $_GET[self::GET_QUERY],
WorkModel::SUMMARY->value => $_GET[self::GET_QUERY]
])->get();
// Return an SQL string from array for use in prepared statements
private static function array_to_wildcard_sql(array $columns): string {
$sql = array_map(fn(string $column): string => "{$column} LIKE CONCAT('%', ?, '%')", $columns);
return implode(" OR ", $sql);
}
// Return chained AND statements from array for use in prepared statements
private static function array_to_and_statement(array $keys): string {
$sql = array_map(fn(string $k): string => "{$k} = ?", $keys);
return implode(" AND ", $sql);
}
// Wildcard search columns in table with query string from query string
// This has to be implemented manually until "libmysqldriver/MySQL" supports wildcard SELECT
private function search(string $table, array $columns, array $conditions = null): array {
// Create CSV from columns array
$columns_concat = implode(",", $columns);
// Create SQL LIKE wildcard statement for each column.
$where = self::array_to_wildcard_sql($columns);
// Create array of values from query string for each colum
$values = array_fill(0, count($columns), $_GET[self::GET_QUERY]);
if ($conditions) {
$conditions_sql = self::array_to_and_statement(array_keys($conditions));
// Wrap positive where statements and prepare new group of conditions
// WHERE (<search_terms>) AND (<conditions>)
$where = "({$where}) AND ({$conditions_sql})";
// Append values from conditions statements to prepared statement
array_push($values, ...array_values($conditions));
}
// Order the rows by the array index of $colums received
$rows = $this->db->exec("SELECT {$columns_concat} FROM {$table} WHERE {$where} ORDER BY {$columns_concat}", $values);
// Return results as assoc or empty array
return parent::is_mysqli_result($rows) ? $rows->fetch_all(MYSQLI_ASSOC) : [];
}
// Search work table
private function search_work(): array {
$search = [
WorkModel::TITLE->value,
WorkModel::SUMMARY->value,
WorkModel::DATE_TIMESTAMP_CREATED->value,
WorkModel::ID->value
];
$conditions = [
WorkModel::IS_LISTABLE->value => true
];
$results = $this->search(WorkModel::TABLE, $search, $conditions);
foreach ($results as &$result) {
$result["actions"] = (new Call(Endpoints::WORK_ACTIONS->value))
->params([WorkActionsModel::ANCHOR->value => $result[WorkModel::ID->value]])
->get()->output();
}
return $results;
}
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
public function main(): Response {
$results = [
Endpoints::WORK->value => $this->search_work()->output()
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
// Get search results for each category
$categories = [
WorkModel::TABLE => $this->search_work()
];
// Calculate the total number of results from all searched endpoints
$num_results = array_sum(array_map(fn(array $result): int => count($result), array_values($results)));
// Count total number of results from all categories
$total_num_results = 0;
foreach (array_values($categories) as $results) {
$total_num_results += count($results);
}
// Return 404 if no search results
return new Response($results, $num_results > 0 ? 200 : 404);
return new Response([
"query" => $_GET[self::GET_QUERY],
"results" => $categories,
"total_num_results" => $total_num_results
]);
}
}

View file

@ -1,19 +1,15 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use const VLW\API\RESP_DELETE_OK;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
require_once Path::root("src/Endpoints.php");
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work.php");
@ -21,47 +17,44 @@
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(WorkModel::ID->value))
$this->ruleset->GET([
(new Rules("id"))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkModel::TITLE->value))
->type(Type::STRING)
->min(3)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkModel::SUMMARY->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_TEXT_MAX_LENGTH),
(new Rules(WorkModel::IS_LISTABLE->value))
->type(Type::BOOLEAN),
(new Rules(WorkModel::IS_READABLE->value))
->type(Type::BOOLEAN),
(new Rules(WorkModel::DATE_MODIFIED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH),
(new Rules(WorkModel::DATE_CREATED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
]);
}
parent::__construct(Databases::VLW, $this->ruleset);
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to delete work data, please try again later", 503);
}
public function main(): Response {
return $this->db->for(FieldsEnumsModel::TABLE)->delete($_POST) === true
? new Response(RESP_DELETE_OK)
: new Response("Failed to delete work entity", 500);
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
// Attempt to update the entity
$update = $this->db->for(WorkModel::TABLE)
->where([WorkModel::ID->value => $_GET["id"]])
->update([
WorkModel::IS_LISTABLE->value => false,
WorkModel::IS_READABLE->value => false
]);
return $update ? new Response($_GET["id"]) : $this->resp_database_error();
}
}

View file

@ -1,98 +1,136 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Method;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsModel;
use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/Work.php");
require_once Path::root("src/databases/models/Work.php");
require_once Path::root("src/databases/models/WorkTags.php");
require_once Path::root("src/databases/models/WorkActions.php");
class GET_Work extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(WorkModel::ID->value))
(new Rules("id"))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkModel::TITLE->value))
->type(Type::STRING)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkModel::SUMMARY->value))
->type(Type::STRING)
->max(parent::MYSQL_TEXT_MAX_LENGTH),
(new Rules(WorkModel::IS_LISTABLE->value))
->type(Type::BOOLEAN)
->default(true),
(new Rules(WorkModel::IS_READABLE->value))
->type(Type::BOOLEAN)
->default(true),
(new Rules(WorkModel::DATE_MODIFIED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH),
(new Rules(WorkModel::DATE_CREATED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
->default(null)
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
public function main(): Response {
// Use copy of search paramters as filters
$filters = $_GET;
// # Helper methods
// Do a wildcard search on the title column if provided
if (array_key_exists(WorkModel::TITLE->value, $_GET)) {
$filters[WorkModel::TITLE->value] = [
"LIKE" => "%{$_GET[WorkModel::TITLE->value]}%"
];
}
private function fetch_row_tags(string $id): array {
$resp = $this->db->for(WorkTagsModel::TABLE)
->where([WorkTagsModel::ANCHOR->value => $id])
->select(WorkTagsModel::NAME->value);
// Do a wildcard search on the summary column if provided
if (array_key_exists(WorkModel::SUMMARY->value, $_GET)) {
$filters[WorkModel::SUMMARY->value] = [
"LIKE" => "%{$_GET[WorkModel::SUMMARY->value]}%"
];
}
return parent::is_mysqli_result($resp) ? $resp->fetch_all(MYSQLI_ASSOC) : [];
}
$response = $this->db->for(WorkModel::TABLE)
->where($filters)
->order([WorkModel::DATE_CREATED->value => "DESC"])
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
private function resp_item_details(string $id): Response {
$resp = $this->db->for(WorkModel::TABLE)
->where([
WorkModel::ID->value => $id,
WorkModel::IS_READABLE->value => true
])
->limit(1)
->select([
WorkModel::ID->value,
WorkModel::TITLE->value,
WorkModel::SUMMARY->value,
WorkModel::IS_LISTABLE->value,
WorkModel::IS_READABLE->value,
WorkModel::COVER_SRCSET->value,
WorkModel::DATE_YEAR->value,
WorkModel::DATE_MONTH->value,
WorkModel::DATE_DAY->value,
WorkModel::DATE_MODIFIED->value,
WorkModel::DATE_CREATED->value
WorkModel::DATE_TIMESTAMP_MODIFIED->value,
WorkModel::DATE_TIMESTAMP_CREATED->value
]);
return $response->num_rows > 0
? new Response($response->fetch_all(MYSQLI_ASSOC))
: new Response([], 404);
// Bail out if something went wrong retrieving rows from the database
if (!parent::is_mysqli_result($resp)) {
return $this->resp_database_error();
}
return $resp->num_rows === 1
? new Response($resp->fetch_assoc())
: new Response("No entity with id '{$id}' was found", 404);
}
public function main(): Response {
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
// Return details about a specific item by id
if (!empty($_GET["id"])) {
return $this->resp_item_details($_GET["id"]);
}
$resp = $this->db->for(WorkModel::TABLE)
->where([WorkModel::IS_LISTABLE->value => true])
->order([WorkModel::DATE_TIMESTAMP_CREATED->value => "DESC"])
->select([
WorkModel::ID->value,
WorkModel::TITLE->value,
WorkModel::SUMMARY->value,
WorkModel::COVER_SRCSET->value,
WorkModel::DATE_YEAR->value,
WorkModel::DATE_MONTH->value,
WorkModel::DATE_DAY->value,
WorkModel::DATE_TIMESTAMP_MODIFIED->value,
WorkModel::DATE_TIMESTAMP_CREATED->value
]);
// Bail out if something went wrong retrieving rows from the database
if (!parent::is_mysqli_result($resp)) {
return $this->resp_database_error();
}
// Resolve foreign keys
$rows = [];
while ($row = $resp->fetch_assoc()) {
$row["tags"] = $this->fetch_row_tags($row["id"]);
// Fetch actions for work entity by id from endpoint
$row["actions"] = (new Call(Endpoints::WORK_ACTIONS->value))
->params([WorkActionsModel::ANCHOR->value => $row[WorkModel::ID->value]])
->get()->output();
$rows[] = $row;
}
return new Response($rows);
}
}

View file

@ -1,31 +1,30 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\{
WorkModel,
WorkPermalinksModel
};
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
use VLW\API\Databases\VLWdb\Models\WorkPermalinks\WorkPermalinksModel;
require_once Path::root("src/Endpoints.php");
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/Work.php");
require_once Path::root("src/databases/models/Work/WorkPermalinks.php");
require_once Path::root("src/databases/models/Work.php");
require_once Path::root("src/databases/models/WorkPermalinks.php");
class PATCH_Work extends VLWdb {
protected Ruleset $ruleset;
protected Response $current_entity;
protected array $updated_entity;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
@ -53,19 +52,19 @@
(new Rules(WorkModel::IS_READABLE->value))
->type(Type::BOOLEAN),
(new Rules(WorkModel::DATE_MODIFIED->value))
(new Rules(WorkModel::DATE_TIMESTAMP_CREATED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH)
->default(time()),
(new Rules(WorkModel::DATE_CREATED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH)
->min(0)
->max(parent::MYSQL_INT_MAX_LENGHT)
]);
parent::__construct();
$this->get_existing_entity();
// Copy all provided post data into a new array
$this->updated_entity = $_POST;
// Set date modified timestamp
$this->updated_entity[WorkModel::DATE_TIMESTAMP_MODIFIED->value] = time();
}
// Generate a slug URL from string
@ -73,46 +72,130 @@
return strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $input)));
}
// Compute and return modeled year, month, and day from Unix timestamp in request body
private static function gen_date_created(): array {
return [
WorkModel::DATE_YEAR->value => date("Y", $_POST[WorkModel::DATE_CREATED->value]),
WorkModel::DATE_MONTH ->value => date("n", $_POST[WorkModel::DATE_CREATED->value]),
WorkModel::DATE_DAY->value => date("j", $_POST[WorkModel::DATE_CREATED->value])
];
// # Helper methods
private function get_existing_entity(): Response {
// Check if an entity already exists with slugified title from GET endpoint
$this->current_entity = Call("work?id={$_GET["id"]}", Method::GET);
// Response is not 404 (Not found) so we can't create the entity
if ($this->current_entity->code !== 200) {
// Response is not a valid entity, something went wrong
if ($this->current_entity->code !== 404) {
return $this->resp_database_error();
}
// Return 402 Conflict
return new Response("No entity with id '{$_GET["id"]}' was found", 404);
}
return $this->current_entity;
}
private function get_entity_by_id(string $id): Response {
return (new Call(Endpoints::WORK->value))->params([
WorkModel::ID->value => $id
])->get();
// Create new permalink for entity slug
private function create_permalink(string $slug): bool {
$create = Call("work/permalinks", Method::POST, [
WorkPermalinksModel::SLUG->value => $slug,
WorkPermalinksModel::ANCHOR->value => $slug
]);
return $create->ok;
}
// ## Updated entity
private function change_slug(): bool {
if (!array_key_exists(WorkModel::ID->value, $this->updated_entity)) {
return true;
}
// Generate new permalink for entity id
return $this->create_permalink($this->updated_entity[WorkModel::ID->value]);
}
private function timestamp_to_dates(): void {
if (!array_key_exists(WorkModel::DATE_TIMESTAMP_CREATED->value, $this->updated_entity)) {
return;
}
// Get timestamp from post data
$timestamp = $this->updated_entity[WorkModel::DATE_TIMESTAMP_CREATED->value];
// Update fractured dates from timestamp
$this->updated_entity[WorkModel::DATE_YEAR->value] = date("Y", $timestamp);
$this->updated_entity[WorkModel::DATE_MONTH ->value] = date("n", $timestamp);
$this->updated_entity[WorkModel::DATE_DAY->value] = date("j", $timestamp);
}
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
// Return a 422 Unprocessable Entity if there is nothing to change
private function resp_no_changes(): Response {
return new Response("No columns to update", 422);
}
// Rollback changes and return error response
private function resp_permalink_error_rollback(): Response {
$update = $this->db->for(WorkModel::TABLE)
->where([WorkModel::ID->value => $_GET["id"]])
->update($this->current_entity->output());
return $update
? new Response("Failed to create new permalink for updated entity. Changes have been rolled back", 500)
: new Reponse("Failed to create new permalink for updated entity. Changes failed to rollback, this is bad.", 500);
}
public function main(): Response {
// Use copy of request body as entity
$entity = $_POST;
// Generate a new slug id from title if changed
if ($_POST[WorkModel::TITLE->value]) {
$slug = $_POST[WorkModel::TITLE->value];
// Bail out if the slug generated from the new tite already exist
if ($this->get_entity_by_id($slug)) {
return new Response("An entity with this title already exist", 409);
}
// Add the new slug to update entity
$entity[WorkModel::ID] = $slug;
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
// Generate new work date fields from timestamp
if ($_POST[WorkModel::DATE_CREATED->value]) {
array_merge($entity, self::gen_date_created());
// Empty payload, nothing to do
if (empty($_POST)) {
return $this->resp_no_changes();
}
// Generate new slug for entity if title is updated
if (array_key_exists(WorkModel::TITLE->value, $_POST)) {
// Generate URL slug from title text or UUID if undefined
$slug = self::gen_slug($_POST["title"]);
// Save generated slug from title if it's different from existing slug
if ($slug !== $this->current_entity->output()[WorkModel::ID->value]) {
$this->updated_entity[WorkModel::ID->value] = $slug;
}
}
// Update fractured dates from timestamp
$this->timestamp_to_dates();
// Attempt to update the entity
$update = $this->db->for(WorkModel::TABLE)
->where([WorkModel::ID->value => $_GET["id"]])
->update($this->updated_entity);
// Bail out if update failed
if (!$update) {
return $this->resp_database_error();
}
// Create new slug for entity if title was changed
if (!$this->change_slug()) {
return $this->resp_permalink_error_rollback();
}
// Update entity by existing id
return $this->db->for(WorkModel::TABLE)->where([WorkModel::ID->value => $_GET[WorkModel::ID->value]])->update($entity) === true
? new Response($_GET[WorkModel::ID->value])
: new Response("Failed to update entity", 500);
// Return 200 OK and new or existing entity slug as body
return new Response($this->current_entity->output()[WorkModel::ID->value]);
}
}

View file

@ -1,31 +1,27 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\{
WorkModel,
WorkPermalinksModel
};
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
use VLW\API\Databases\VLWdb\Models\WorkPermalinks\WorkPermalinksModel;
require_once Path::root("src/Endpoints.php");
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/Work.php");
require_once Path::root("src/databases/models/Work/WorkPermalinks.php");
require_once Path::root("src/databases/models/Work.php");
require_once Path::root("src/databases/models/WorkPermalinks.php");
class POST_Work extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
@ -41,22 +37,12 @@
->max(parent::MYSQL_TEXT_MAX_LENGTH)
->default(null),
(new Rules(WorkModel::IS_LISTABLE->value))
->type(Type::BOOLEAN)
->default(false),
(new Rules(WorkModel::IS_READABLE->value))
->type(Type::BOOLEAN)
->default(false),
(new Rules(WorkModel::DATE_CREATED->value))
(new Rules(WorkModel::DATE_TIMESTAMP_CREATED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH)
->default(time())
->max(parent::MYSQL_INT_MAX_LENGHT)
->default(null)
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
// Generate a slug URL from string
@ -64,51 +50,84 @@
return strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $input)));
}
// Compute and return modeled year, month, and day from a Unix timestamp
private static function gen_date_created(): array {
// Use provided timestamp in request
$date_created = $_POST[WorkModel::DATE_CREATED->value];
// Create permalink for entity slug
private function create_permalink(string $slug): bool {
$create = Call("work/permalinks", Method::POST, [
WorkPermalinksModel::SLUG->value => $slug,
WorkPermalinksModel::ANCHOR->value => $slug
]);
return [
WorkModel::DATE_YEAR->value => date("Y", $date_created),
WorkModel::DATE_MONTH ->value => date("n", $date_created),
WorkModel::DATE_DAY->value => date("j", $date_created)
];
return $create->ok;
}
private function get_entity_by_id(string $id): Response {
return (new Call(Endpoints::WORK->value))->params([
WorkModel::ID->value => $id
])->get();
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
public function main(): Response {
// Use copy of request body as entity
$entity = $_POST;
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
// Generate URL slug from title text or UUID if undefined
$entity[WorkModel::ID->value] = $_POST[WorkModel::TITLE->value]
? self::gen_slug($_POST[WorkModel::TITLE->value])
: parent::gen_uuid4();
$slug = !empty($_POST["title"]) ? self::gen_slug($_POST["title"]) : parent::gen_uuid4();
// Bail out here if a work entry with id had been created already
if ($this->get_entity_by_id($entity[WorkModel::ID->value])->ok) {
return new Response("An entity with id '{$slug}' already exist", 409);
// Check if an entity already exists with slugified title from GET endpoint
$existing_entity = Call("work?id={$slug}", Method::GET);
// Response is not 404 (Not found) so we can't create the entity
if ($existing_entity->code !== 404) {
// Response is not a valid entity, something went wrong
if ($existing_entity->code !== 200) {
return $this->resp_database_error();
}
// Return 402 Conflict
return new Response("Entity with id '{$slug}' already exists", 402);
}
// Generate the necessary date fields
array_merge($entity, self::gen_date_created());
// Get created timestamp from payload or use current time if not specified
$created_timestamp = $_POST[WorkModel::DATE_TIMESTAMP_CREATED->value]
? $_POST[WorkModel::DATE_TIMESTAMP_CREATED->value]
: time();
// Let's try to insert the new entity
if (!$this->db->for(WorkModel::TABLE)->insert($entity)) {
return new Response("Failed to insert work entry", 500);
// Attempt to create new entity
$insert = $this->db->for(WorkModel::TABLE)
->insert([
WorkModel::ID->value => $slug,
WorkModel::TITLE->value => $_POST["title"],
WorkModel::SUMMARY->value => $_POST["summary"],
WorkModel::IS_LISTABLE->value => true,
WorkModel::IS_READABLE->value => true,
WorkModel::DATE_YEAR->value => date("Y", $created_timestamp),
WorkModel::DATE_MONTH ->value => date("n", $created_timestamp),
WorkModel::DATE_DAY->value => date("j", $created_timestamp),
WorkModel::DATE_TIMESTAMP_MODIFIED->value => null,
WorkModel::DATE_TIMESTAMP_CREATED->value => $created_timestamp,
]);
// Bail out if insert failed
if (!$insert) {
return $this->resp_database_error();
}
// Generate permalink for new entity
return (new Call(Endpoints::WORK_PERMALINKS->value))->post([
WorkPermalinksModel::ID => $entity[WorkModel::ID->value],
WorkPermalinksModel::REF_WORK_ID => $entity[WorkModel::ID->value],
WorkPermalinksModel::DATE_CREATED => time()
]);
// Create permalink for new entity
if (!$this->create_permalink($slug)) {
// Rollback created entity if permalink creation failed
Call("work", Method::DELETE, [WorkModel::ID->value => $slug]);
return new Response("Failed to create permalink", 500);
}
// Return 201 Created and entity slug as body
return new Response($slug, 201);
}
}

View file

@ -6,11 +6,10 @@
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use const VLW\API\RESP_DELETE_OK;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel;
require_once Path::root("src/databases/VLWdb.php");
@ -24,15 +23,51 @@
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(WorkActionsModel::REF_WORK_ID->value))
(new Rules("id"))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
]);
}
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
public function main(): Response {
return $this->db->for(WorkActionsModel::TABLE)->delete($_POST) === true
? new Response(RESP_DELETE_OK)
: new Response("Failed to delete action for work entity", 500);
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
// Ensure the action exists by id
$existing_action = $this->db->for(WorkActionsModel::TABLE)
->where([
WorkActionsModel::ID->value => $_POST["id"]
])
->select(null);
// Return idempotent deletion if the action does not exist
if ($existing_action->num_rows === 0) {
return new Response($_POST["id"]);
}
// Attempt to delete action by id
$delete = $this->db->for(WorkActionsModel::TABLE)
->delete([
WorkActionsModel::ID->value => $_POST["id"]
]);
// Return 201 Created and entity id as body if insert was successful
return $delete === true ? new Response($_POST["id"], 201) : $this->resp_database_error();
}
}

View file

@ -6,44 +6,62 @@
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/WorkActions.php");
require_once Path::root("src/databases/models/WorkActions.php");
class GET_WorkActions extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(WorkActionsModel::REF_WORK_ID->value))
(new Rules(WorkActionsModel::ANCHOR->value))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
]);
}
parent::__construct(Databases::VLW, $this->ruleset);
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
public function main(): Response {
$response = $this->db->for(WorkActionsModel::TABLE)
->where($_GET)
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
$resp = $this->db->for(WorkActionsModel::TABLE)
->where([WorkActionsModel::ANCHOR->value => $_GET[WorkActionsModel::ANCHOR->value]])
->select([
WorkActionsModel::REF_WORK_ID->value,
WorkActionsModel::DISPLAY_TEXT->value,
WorkActionsModel::HREF->value,
WorkActionsModel::CLASS_LIST->value,
WorkActionsModel::EXTERNAL->value
]);
return $response->num_rows > 0
? new Response($response->fetch_all(MYSQLI_ASSOC))
: new Response([], 404);
// Bail out if something went wrong retrieving rows from the database
if (!parent::is_mysqli_result($resp)) {
return $this->resp_database_error();
}
return $resp->num_rows > 0
? new Response($resp->fetch_all(MYSQLI_ASSOC))
: new Response([]);
}
}

View file

@ -1,39 +1,36 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\{
WorkModel,
WorkActionsModel
};
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel;
require_once Path::root("src/Endpoints.php");
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/Work.php");
require_once Path::root("src/databases/models/Work/WorkActions.php");
require_once Path::root("src/databases/models/WorkActions.php");
class POST_WorkActions extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(WorkActionsModel::REF_WORK_ID->value))
$this->ruleset->GET([
(new Rules("id"))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
]);
$this->ruleset->POST([
(new Rules(WorkActionsModel::DISPLAY_TEXT->value))
->required()
->type(Type::STRING)
@ -50,31 +47,56 @@
(new Rules(WorkActionsModel::CLASS_LIST->value))
->type(Type::ARRAY)
->min(1)
->max(4)
->default([]),
(new Rules(WorkActionsModel::EXTERNAL->value))
->type(Type::BOOLEAN)
->default(false)
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
private static function get_entity(): Response {
return (new Call(Endpoints::WORK->value))->params([
WorkModel::ID->value => $_POST[WorkActionsModel::REF_WORK_ID->value]
])->get();
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
public function main(): Response {
// Bail out if work entity could not be fetched
$entity = self::get_entity();
if (!$entity->ok) {
return $entity;
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
return $this->db->for(WorkActionsModel::TABLE)->insert($_POST) === true
? new Response($_POST[WorkActionsModel::REF_WORK_ID->value], 201)
: new Response("Failed to add action to work entity", 500);
// Ensure an entity with the provided id exists
$entity = Call("work?id={$_GET["id"]}", Method::GET);
if ($entity->code !== 200) {
// Response from endpoint is not 404, something went wrong
if ($entity->code !== 404) {
return $this->resp_database_error();
}
return new Response("No entity with id '{$_GET["id"]}' was found", 404);
}
// Attempt to create action for entity
$insert = $this->db->for(WorkActionsModel::TABLE)
->insert([
WorkActionsModel::ID->value => parent::gen_uuid4(),
WorkActionsModel::ANCHOR->value => $_GET["id"],
WorkActionsModel::DISPLAY_TEXT->value => $_POST[WorkActionsModel::DISPLAY_TEXT->value],
WorkActionsModel::HREF->value => $_POST[WorkActionsModel::HREF->value],
WorkActionsModel::CLASS_LIST->value => implode(",", $_POST[WorkActionsModel::CLASS_LIST->value]),
WorkActionsModel::EXTERNAL->value => $_POST[WorkActionsModel::EXTERNAL->value],
]);
// Return 201 Created and entity id as body if insert was successful
return $insert === true ? new Response($_GET["id"], 201) : $this->resp_database_error();
}
}

View file

@ -1,52 +1,60 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\WorkPermalinksModel;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\WorkPermalinks\WorkPermalinksModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/WorkPermalinks.php");
require_once Path::root("src/databases/models/WorkPermalinks.php");
class GET_WorkPermalinks extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(WorkPermalinksModel::ID->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkPermalinksModel::REF_WORK_ID->value))
(new Rules("id"))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
]);
}
parent::__construct(Databases::VLW, $this->ruleset);
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to resolve permalink, please try again later", 503);
}
public function main(): Response {
$response = $this->db->for(WorkPermalinksModel::TABLE)
->where($_GET)
->select([
WorkPermalinksModel::ID->value,
WorkPermalinksModel::REF_WORK_ID->value,
WorkPermalinksModel::DATE_CREATED->value
]);
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
return $response->num_rows > 0
? new Response($response->fetch_all(MYSQLI_ASSOC))
: new Response([], 404);
// Get all anchors that match the requested slug
$resolve = $this->db->for(WorkPermalinksModel::TABLE)
->where([WorkPermalinksModel::SLUG->value => $_GET["id"]])
->select(WorkPermalinksModel::ANCHOR->value);
// Return array of all matched work table ids. Or empty array if none found
return parent::is_mysqli_result($resolve)
? new Response(array_column($resolve->fetch_all(MYSQLI_ASSOC), WorkPermalinksModel::ANCHOR->value))
: $this->resp_database_error();
}
}

View file

@ -1,65 +1,83 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\WorkPermalinksModel;
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\WorkPermalinks\WorkPermalinksModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/WorkPermalinks.php");
require_once Path::root("src/databases/models/WorkPermalinks.php");
class POST_WorkPermalinks extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(WorkPermalinksModel::ID->value))
(new Rules("slug"))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkPermalinksModel::REF_WORK_ID->value))
(new Rules("anchor"))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkPermalinksModel::DATE_CREATED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH)
->default(time())
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
private static function get_entity(): Response {
return (new Call(Endpoints::WORK->value))->params([
WorkModel::ID->value => $_POST[WorkTagsModel::REF_WORK_ID->value]
])->get();
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to resolve permalink, please try again later", 503);
}
public function main(): Response {
// Bail out if work entity could not be fetched
$entity = self::get_entity();
if (!$entity->ok) {
return $entity;
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
return $this->db->for(WorkPermalinksModel::TABLE)->insert($_POST) === true
? new Response($_POST[WorkPermalinksModel::ID->value], 201)
: new Response("Failed to add permalink to work entity", 500);
// Check if an entity exists with slug
$existing_entity = Call("work?id={$_POST["slug"]}", Method::GET);
// Response is not 404 (Not found) so we can't create the entity
if ($existing_entity->code !== 200) {
// Response is not a valid entity, something went wrong
if ($existing_entity->code !== 404) {
return $this->resp_database_error();
}
// Return 402 Conflict
return new Response("No work entity with id '{$_POST["slug"]}' was found to permalink", 404);
}
// Attempt to create new entity
$insert = $this->db->for(WorkPermalinksModel::TABLE)
->insert([
WorkPermalinksModel::SLUG->value => $_POST["slug"],
WorkPermalinksModel::ANCHOR->value => $_POST["anchor"],
WorkPermalinksModel::DATE_TIMESTAMP_CREATED->value => time(),
]);
// Return 201 Created and entity slug as body if insert was successful
return $insert === true ? new Response($_POST["slug"], 201) : $this->resp_database_error();
}
}

View file

@ -6,37 +6,75 @@
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use const VLW\API\RESP_DELETE_OK;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsModel;
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsNameEnum;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/WorkTags.php");
require_once Path::root("src/databases/models/WorkTags.php");
class DELETE_WorkTags extends VLWdb {
private Ruleset $ruleset;
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(WorkTagsModel::REF_WORK_ID->value))
$this->ruleset->POST([
(new Rules("id"))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkTagsModel::NAME->value))
->required()
->type(Type::ENUM, WorkTagsNameEnum::names())
]);
}
parent::__construct(Databases::VLW, $this->ruleset);
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
public function main(): Response {
return $this->db->for(WorkTagsModel::TABLE)->delete($_POST) === true
? new Response(RESP_DELETE_OK)
: new Response("Failed to delete value from document", 500);
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
// Ensure the tag exists for entity id
$existing_tag = $this->db->for(WorkTagsModel::TABLE)
->where([
WorkTagsModel::ANCHOR->value => $_POST["id"],
WorkTagsModel::NAME->value => $_POST["name"]
])
->select(null);
// Return idempotent deletion if the tag does not exist
if ($existing_tag->num_rows === 0) {
return new Response($_POST["id"]);
}
// Attempt to delete tag for entity
$delete = $this->db->for(WorkTagsModel::TABLE)
->delete([
WorkTagsModel::ANCHOR->value => $_POST["id"],
WorkTagsModel::NAME->value => $_POST["name"]
]);
// Return 201 Created and entity id as body if insert was successful
return $delete === true ? new Response($_POST["id"], 201) : $this->resp_database_error();
}
}

View file

@ -1,51 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\{
WorkTagsModel,
WorkTagsNameEnum
};
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/WorkTags.php");
class GET_WorkTags extends VLWdb {
private Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(WorkTagsModel::REF_WORK_ID->value))
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkTagsModel::NAME->value))
->type(Type::ENUM, WorkTagsNameEnum::names())
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
public function main(): Response {
$response = $this->db->for(WorkTagsModel::TABLE)
->where($_GET)
->select([
WorkTagsModel::REF_WORK_ID->value,
WorkTagsModel::NAME->value
]);
return $response->num_rows > 0
? new Response($response->fetch_all(MYSQLI_ASSOC))
: new Response([], 404);
}
}

View file

@ -1,63 +1,93 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\{
WorkModel,
WorkTagsModel,
WorkTagsNameEnum
};
use Reflect\Method;
use function Reflect\Call;
use VLW\API\Databases\VLWdb\VLWdb;
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsModel;
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsNameEnum;
require_once Path::root("src/Endpoints.php");
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/Work.php");
require_once Path::root("src/databases/models/Work/WorkTags.php");
require_once Path::root("src/databases/models/WorkTags.php");
class POST_WorkTags extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(WorkTagsModel::REF_WORK_ID->value))
$this->ruleset->GET([
(new Rules("id"))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
]);
$this->ruleset->POST([
(new Rules(WorkTagsModel::NAME->value))
->required()
->type(Type::ENUM, WorkTagsNameEnum::names())
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
private static function get_entity(): Response {
return (new Call(Endpoints::WORK->value))->params([
WorkModel::ID->value => $_POST[WorkTagsModel::REF_WORK_ID->value]
])->get();
// # Responses
// Return 422 Unprocessable Content error if request validation failed
private function resp_rules_invalid(): Response {
return new Response($this->ruleset->get_errors(), 422);
}
// Return a 503 Service Unavailable error if something went wrong with the database call
private function resp_database_error(): Response {
return new Response("Failed to get work data, please try again later", 503);
}
public function main(): Response {
// Bail out if work entity could not be fetched
$entity = self::get_entity();
if (!$entity->ok) {
return $entity;
// Bail out if request validation failed
if (!$this->ruleset->is_valid()) {
return $this->resp_rules_invalid();
}
return $this->db->for(WorkTagsModel::TABLE)->insert($_POST) === true
? new Response($_POST[WorkTagsModel::REF_WORK_ID->value], 201)
: new Response("Failed to add tag to work entity", 500);
// Ensure an entity with the provided id exists
$entity = Call("work?id={$_GET["id"]}", Method::GET);
if ($entity->code !== 200) {
// Response from endpoint is not 404, something went wrong
if ($entity->code !== 404) {
return $this->resp_database_error();
}
return new Response("No entity with id '{$_GET["id"]}' was found", 404);
}
// Ensure the tag does not already exist for entity
$existing_tag = $this->db->for(WorkTagsModel::TABLE)
->where([
WorkTagsModel::ANCHOR->value => $_GET["id"],
WorkTagsModel::NAME->value => $_POST["name"]
])
->select(null);
// Bail out if this tag already exists
if ($existing_tag->num_rows !== 0) {
return new Response("Tag '{$_POST["name"]}' is already set on entity id '{$_GET["id"]}'", 402);
}
// Attempt to create tag for entity
$insert = $this->db->for(WorkTagsModel::TABLE)
->insert([
WorkTagsModel::ANCHOR->value => $_GET["id"],
WorkTagsModel::NAME->value => $_POST["name"]
]);
// Return 201 Created and entity id as body if insert was successful
return $insert === true ? new Response($_GET["id"], 201) : $this->resp_database_error();
}
}

View file

@ -1,2 +0,0 @@
# Install dependencies
composer install --optimize-autoloader

View file

@ -1,29 +0,0 @@
<?php
namespace VLW\API;
// Default string to return when a DELETE request is successful
const RESP_DELETE_OK = "OK";
// Enum of all available VLW endpoints grouped by category
enum Endpoints: string {
case SEARCH = "/search";
case MESSAGES = "/messages";
case NOTES = "/notes";
case WORK = "/work";
case WORK_TAGS = "/work/tags";
case WORK_ACTIONS = "/work/actions";
case BATTLESTATION = "/battlestation";
case BATTLESTATION_MB = "/battlestation/mb";
case BATTLESTATION_CPU = "/battlestation/cpu";
case BATTLESTATION_GPU = "/battlestation/gpu";
case BATTLESTATION_PSU = "/battlestation/psu";
case BATTLESTATION_DRAM = "/battlestation/dram";
case BATTLESTATION_STORAGE = "/battlestation/storage";
case BATTLESTATION_COOLERS = "/battlestation/coolers";
case BATTLESTATION_CHASSIS = "/battlestation/chassis";
}

View file

@ -2,38 +2,24 @@
namespace VLW\API\Databases\VLWdb;
use Reflect\Path;
use Reflect\Request;
use Reflect\Response;
use ReflectRules\Ruleset;
use vlw\MySQL\MySQL;
enum Databases: string {
case VLW = "vlw";
case BATTLESTATION = "battlestation";
}
use libmysqldriver\MySQL;
class VLWdb {
const UUID_LENGTH = 36;
const MYSQL_INT_MAX_LENGTH = 2147483647;
const MYSQL_TEXT_MAX_LENGTH = 65538;
const MYSQL_TEXT_MAX_LENGTH = 65538;
const MYSQL_VARCHAR_MAX_LENGTH = 255;
const MYSQL_TINYINT_MAX_LENGTH = 255;
const MYSQL_INT_MAX_LENGHT = 2147483647;
protected readonly MySQL $db;
public function __construct(Databases $database, Ruleset $ruleset) {
// Validate provided Ruleset before attempting to connect to the database
self::eval_ruleset_or_exit($ruleset);
protected MySQL $db;
public function __construct() {
// Create new MariaDB connection
$this->db = new MySQL(
$_ENV["connect"]["host"],
$_ENV["connect"]["user"],
$_ENV["connect"]["pass"],
$_ENV["databases"][$database->value],
$_ENV["vlwdb"]["mariadb_host"],
$_ENV["vlwdb"]["mariadb_user"],
$_ENV["vlwdb"]["mariadb_pass"],
$_ENV["vlwdb"]["mariadb_db"],
);
}
@ -60,24 +46,7 @@
);
}
// Mutate the value by array key $property_name into a libmysqldriver\MySQL custom operator
// https://codeberg.org/vlw/php-mysql#define-custom-operators
public static function make_wildcard_search(string $property_name, array &$filters): array {
// Bail out if property name is not set in filters array or if its value is null
if (!array_key_exists($property_name, $filters) || $filters[$property_name] === null) {
return $filters;
}
// Mutate filter value into a custom operator array
$filters[$property_name] = [
"LIKE" => "%{$filters[$property_name]}%"
];
return $filters;
}
// Bail out if provided ReflectRules\Ruleset is invalid
private static function eval_ruleset_or_exit(Ruleset $ruleset): ?Response {
return !$ruleset->is_valid() ? new Response($ruleset->get_errors(), 422) : null;
public static function is_mysqli_result(\mysqli_result|bool $resp): bool {
return $resp instanceof \mysqli_result;
}
}

View file

@ -1,19 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation;
use victorwesterlund\xEnum;
enum ChassisModel: string {
use xEnum;
const TABLE = "chassis";
case ID = "id";
case VENDOR_NAME = "vendor_name";
case VENDOR_MODEL = "vendor_model";
case STORAGE_TWOINCHFIVE = "storage_2i5hi";
case STORAGE_THREEINCHFIVE = "storage_3i5hi";
case DATE_AQUIRED = "date_aquired";
case IS_RETIRED = "is_retired";
}

View file

@ -1,14 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation\Config;
use victorwesterlund\xEnum;
enum ChassisMbModel: string {
use xEnum;
const TABLE = "config_chassis_mb";
case REF_CHASSIS_ID = "ref_chassis_id";
case REF_MB_ID = "ref_mb_id";
}

View file

@ -1,15 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation\Config;
use victorwesterlund\xEnum;
enum ConfigModel: string {
use xEnum;
const TABLE = "config";
case REF_MB_ID = "ref_mb_id";
case FRIENDLY_NAME = "friendly_name";
case DATE_BUILT = "date_built";
}

View file

@ -1,24 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation\Config;
use victorwesterlund\xEnum;
enum SocketTypeEnum {
use xEnum;
case SLOTTED;
case INTEGRATED;
}
enum MbCpuCoolerModel: string {
use xEnum;
const TABLE = "config_mb_cpu_cooler";
case REF_MB_ID = "ref_mb_id";
case REF_CPU_ID = "ref_cpu_id";
case REF_COOLER_ID = "ref_cooler_id";
case SOCKET = "socket";
case SOCKET_TYPE = "socket_type";
}

View file

@ -1,23 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation\Config;
use victorwesterlund\xEnum;
enum SocketTypeModel {
use xEnum;
case SLOTTED;
case INTEGRATED;
}
enum MbDramModel: string {
use xEnum;
const TABLE = "config_mb_dram";
case REF_MB_ID = "ref_mb_id";
case REF_DRAM_ID = "ref_dram_id";
case SOCKET = "socket";
case SOCKET_TYPE = "socket_type";
}

View file

@ -1,14 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation\Config;
use victorwesterlund\xEnum;
enum MbGpuModel: string {
use xEnum;
const TABLE = "config_mb_gpu";
case REF_MB_ID = "ref_mb_id";
case REF_GPU_ID = "ref_gpu_id";
}

View file

@ -1,14 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation\Config;
use victorwesterlund\xEnum;
enum MbPsuModel: string {
use xEnum;
const TABLE = "config_mb_psu";
case REF_MB_ID = "ref_mb_id";
case REF_PSU_ID = "ref_psu_id";
}

View file

@ -1,25 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation\Config;
use victorwesterlund\xEnum;
enum MbStorageSlotFormfactorEnum: string {
use xEnum;
case TWODOTFIVE = "2.5";
case THREEDOTFIVE = "3.5";
case MDOTTWO = "M.2";
case EXTERNAL = "EXTERNAL";
}
enum MbStorageModel: string {
use xEnum;
const TABLE = "config_mb_storage";
case REF_MB_ID = "ref_mb_id";
case REF_STORAGE_ID = "ref_storage_id";
case INTERFACE = "interface";
case SLOT_FORMFACTOR = "slot_formfactor";
}

View file

@ -1,16 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation;
enum CoolersModel: string {
const TABLE = "coolers";
case ID = "id";
case TYPE_LIQUID = "type_liquid";
case SIZE_FAN = "size_fan";
case SIZE_RADIATOR = "size_radiator";
case VENDOR_NAME = "vendor_name";
case VENDOR_MODEL = "vendor_model";
case DATE_AQUIRED = "date_aquired";
case IS_RETIRED = "is_retired";
}

View file

@ -1,31 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation;
use victorwesterlund\xEnum;
enum ClassEnum {
use xEnum;
case DESKTOP;
case LAPTOP;
case SERVER;
}
enum CpuModel: string {
use xEnum;
const TABLE = "cpu";
case ID = "id";
case CPU_CLASS = "class";
case CLOCK_BASE = "clock_base";
case CLOCK_TURBO = "clock_turbo";
case CORE_COUNT_PERFORMANCE = "core_count_performance";
case CORE_COUNT_EFFICIENCY = "core_count_efficiency";
case CORE_THREADS = "core_threads";
case VENDOR_NAME = "vendor_name";
case VENDOR_MODEL = "vendor_model";
case DATE_AQUIRED = "date_aquired";
case IS_RETIRED = "is_retired";
}

View file

@ -1,37 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation;
use victorwesterlund\xEnum;
enum DramFormfactorEnum {
use xEnum;
case DIMM;
case SODIMM;
}
enum DramTechnologyEnum {
use xEnum;
case DDR4;
case DDR5;
}
enum DramModel: string {
use xEnum;
const TABLE = "dram";
case ID = "id";
case CAPACITY = "capacity";
case SPEED = "speed";
case FORMFACTOR = "formfactor";
case TECHNOLOGY = "technology";
case ECC = "ecc";
case BUFFERED = "buffered";
case VENDOR_NAME = "vendor_name";
case VENDOR_MODEL = "vendor_model";
case DATE_AQUIRED = "date_aquired";
case IS_RETIRED = "is_retired";
}

View file

@ -1,20 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation;
use victorwesterlund\xEnum;
enum GpuModel: string {
use xEnum;
const TABLE = "gpu";
case ID = "id";
case MEMORY = "memory";
case VENDOR_NAME = "vendor_name";
case VENDOR_MODEL = "vendor_model";
case VENDOR_CHIP_NAME = "vendor_chip_name";
case VENDOR_CHIP_MODEL = "vendor_chip_model";
case DATE_AQUIRED = "date_aquired";
case IS_RETIRED = "is_retired";
}

View file

@ -1,30 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation;
use victorwesterlund\xEnum;
enum MbFormfactorEnum {
use xEnum;
case ATX;
case MTX;
case ITX;
case LAPTOP;
}
enum MbModel: string {
use xEnum;
const TABLE = "mb";
case ID = "id";
case FORMFACTOR = "formfactor";
case VENDOR_NAME = "vendor_name";
case VENDOR_MODEL = "vendor_model";
case NETWORK_ETHERNET = "network_ethernet";
case NETWORK_WLAN = "network_wlan";
case NETWORK_BLUETOOTH = "network_bluetooth";
case DATE_AQUIRED = "date_aquired";
case IS_RETIRED = "is_retired";
}

View file

@ -1,31 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation;
use victorwesterlund\xEnum;
enum EightyplusRatingEnum {
use xEnum;
case BASE;
case BRONZE;
case SILVER;
case GOLD;
case PLATINUM;
case TITANIUM;
}
enum PsuModel: string {
use xEnum;
const TABLE = "psu";
case ID = "id";
case POWER = "power";
case VENDOR_NAME = "vendor_name";
case VENDOR_MODEL = "vendor_model";
case TYPE_MODULAR = "type_modular";
case EIGHTYPLUS_RATING = "80plus_rating";
case DATE_AQUIRED = "date_aquired";
case IS_RETIRED = "is_retired";
}

View file

@ -1,45 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Battlestation;
use victorwesterlund\xEnum;
enum StorageDiskTypeEnum {
use xEnum;
case SSD;
case HDD;
}
enum StorageDiskInterfaceEnum {
use xEnum;
case SATA;
case NVME;
case USB;
}
enum StorageDiskFormfactorEnum{
use xEnum;
case TWODOTFIVE;
case THREEDOTFIVE;
case MDOTTWO;
}
enum StorageModel: string {
use xEnum;
const TABLE = "storage";
case ID = "id";
case DISK_TYPE = "disk_type";
case DISK_SIZE = "disk_size";
case DISK_SECTORS = "disk_sectors";
case DISK_INTERFACE = "disk_interface";
case DISK_FORMFACTOR = "disk_formfactor";
case VENDOR_NAME = "vendor_name";
case VENDOR_MODEL = "vendor_model";
case DATE_AQUIRED = "date_aquired";
case IS_RETIRED = "is_retired";
}

View file

@ -0,0 +1,10 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Coffee;
enum CoffeeModel: string {
const TABLE = "coffee";
case ID = "id";
case DATE_TIMESTAMP_CREATED = "date_timestamp_created";
}

View file

@ -0,0 +1,24 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Media;
use victorwesterlund\xEnum;
enum MediaTypeEnum: string {
use xEnum;
case BLOB = "BLOB";
case IMAGE = "IMAGE";
}
enum MediaModel: string {
const TABLE = "media";
case ID = "id";
case NAME = "name";
case TYPE = "type";
case MIME = "mime";
case EXTENSION = "extension";
case SRCSET = "srcset";
case DATE_TIMESTAMP_CREATED = "date_timestamp_created";
}

View file

@ -0,0 +1,10 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\MediaSrcset;
enum MediaSrcsetModel: string {
const TABLE = "media_srcset";
case ID = "id";
case ANCHOR_DEFAULT = "anchor_default";
}

View file

@ -0,0 +1,15 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Messages;
enum MessagesModel: string {
const TABLE = "messages";
case ID = "id";
case EMAIL = "email";
case MESSAGE = "message";
case IS_READ = "is_read";
case IS_SPAM = "is_spam";
case IS_SAVED = "is_saved";
case DATE_TIMESTAMP_CREATED = "date_timestamp_created";
}

View file

@ -1,15 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Messages;
enum MessagesModel: string {
const TABLE = "messages";
case ID = "id";
case EMAIL = "email";
case MESSAGE = "message";
case IS_READ = "is_read";
case IS_SPAM = "is_spam";
case IS_SAVED = "is_saved";
case DATE_CREATED = "date_created";
}

View file

@ -0,0 +1,19 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Work;
enum WorkModel: string {
const TABLE = "work";
case ID = "id";
case TITLE = "title";
case SUMMARY = "summary";
case COVER_SRCSET = "cover_srcset";
case IS_LISTABLE = "is_listable";
case IS_READABLE = "is_readable";
case DATE_YEAR = "date_year";
case DATE_MONTH = "date_month";
case DATE_DAY = "date_day";
case DATE_TIMESTAMP_MODIFIED = "date_timestamp_modified";
case DATE_TIMESTAMP_CREATED = "date_timestamp_created";
}

View file

@ -1,19 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Work;
enum WorkModel: string {
const TABLE = "work";
case ID = "id";
case TITLE = "title";
case SUMMARY = "summary";
case COVER_SRCSET = "cover_srcset";
case IS_LISTABLE = "is_listable";
case IS_READABLE = "is_readable";
case DATE_YEAR = "date_year";
case DATE_MONTH = "date_month";
case DATE_DAY = "date_day";
case DATE_MODIFIED = "date_modified";
case DATE_CREATED = "date_created";
}

View file

@ -1,20 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Work;
use victorwesterlund\xEnum;
enum WorkTagsNameEnum {
use xEnum;
case VLW;
case RELEASE;
case WEBSITE;
}
enum WorkTagsModel: string {
const TABLE = "work_tags";
case REF_WORK_ID = "ref_work_id";
case NAME = "name";
}

View file

@ -5,7 +5,8 @@
enum WorkActionsModel: string {
const TABLE = "work_actions";
case REF_WORK_ID = "ref_work_id";
case ID = "id";
case ANCHOR = "anchor";
case DISPLAY_TEXT = "display_text";
case HREF = "href";
case CLASS_LIST = "class_list";

View file

@ -1,6 +1,6 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Work;
namespace VLW\API\Databases\VLWdb\Models\WorkPermalinks;
enum WorkPermalinksModel: string {
const TABLE = "work_permalinks";

View file

@ -0,0 +1,20 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Work;
use victorwesterlund\xEnum;
enum WorkTagsNameEnum: string {
use xEnum;
case VLW = "VLW";
case RELEASE = "RELEASE";
case WEBSITE = "WEBSITE";
}
enum WorkTagsModel: string {
const TABLE = "work_tags";
case ANCHOR = "anchor";
case NAME = "name";
}

View file

@ -0,0 +1,17 @@
{
"name": "local/api.endpoints",
"description": "Endpoint pathmappings for VLW API",
"type": "library",
"version": "1.0.0-dev",
"authors": [
{
"name": "Victor Westerlund",
"email": "victor@vlw.se"
}
],
"autoload": {
"psr-4": {
"VLW\\API\\": "src/"
}
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace VLW\API;
enum Endpoints: string {
case WORK = "/work";
case SEARCH = "/search";
case MESSAGES = "/messages";
case WORK_TAGS = "/work/tags";
case WORK_ACTIONS = "/work/actions";
}

327
assets/css/document.css Executable file
View file

@ -0,0 +1,327 @@
:root {
--primer-color-accent: 255, 255, 0;
--color-accent: yellow;
--hue-accent: 0deg;
--padding: 20px;
--running-size: 80px;
}
/* # Cornerstones */
* {
margin: 0;
box-sizing: border-box;
font-family: "Roboto Mono", sans-serif;
color: inherit;
font-size: inherit;
}
::-webkit-scrollbar {
display: none;
}
body {
display: grid;
justify-items: center;
grid-template-rows: var(--running-size) 1fr;
overscroll-behavior: none;
background-color: black;
color: white;
overflow-x: hidden;
min-height: 100svh;
font-size: 15px;
}
body.search-dialog-open {
overflow: hidden;
}
a {
display: contents;
color: inherit;
text-decoration: none;
}
/* # Components */
:is(h1, h2, h3, p, li) > a {
--underline-tickness: 3px;
display: initial;
text-decoration: underline;
text-decoration-thickness: var(--underline-tickness);
text-underline-offset: var(--underline-tickness);
text-decoration-color: var(--color-accent);
}
h1 {
font-size: 30px;
color: var(--color-accent);
}
h2 {
font-size: 30px;
}
h3 {
font-size: 25px;
}
/* ## Buttons */
button {
padding: calc(var(--padding) / 2) var(--padding);
color: white;
border: solid 2px white;
border-radius: 6px;
background-color: transparent;
cursor: pointer;
}
button.solid {
color: black;
border-color: var(--color-accent);
background-color: var(--color-accent);
}
a > button::after {
content: " ➜";
}
a[target="_blank"] > button::after,
:is(h1, h2, h3, p, li) > a[target="_blank"]::after {
content: " ↑";
color: var(--color-accent);
white-space: nowrap;
}
a > button.solid:not(:hover)::after {
color: black;
}
/* ## Header */
header {
--border-style: solid 1px rgba(255, 255, 255, .2);
position: sticky;
top: 0;
width: 100%;
height: var(--running-size);
border-bottom: var(--border-style);
display: grid;
align-items: stretch;
justify-items: end;
grid-template-columns: 1fr var(--running-size);
grid-template-rows: var(--running-size);
background-color: rgba(0, 0, 0, .8);
z-index: 100;
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
}
header nav {
display: flex;
align-items: center;
padding: var(--padding);
}
header .logo {
width: calc(var(--running-size) - 1px);
height: calc(var(--running-size) - 1px);
display: grid;
align-items: center;
justify-items: center;
border-left: var(--border-style);
}
header .logo path.stroke {
fill: var(--color-accent);
}
header searchbox {
display: none;
}
/* ## Main */
main {
transition: 400ms transform;
position: relative;
padding: calc(var(--padding) * 1.5);
width: 100%;
max-width: 1000px;
}
main > * {
transition: 100ms opacity;
opacity: 1;
}
main.loading > * {
opacity: 0;
}
/* ## Search */
/* ### Box */
searchbox {
--icon-size: 25px;
display: grid;
width: 100%;
border-left: var(--border-style);
grid-template-columns: var(--icon-size) 1fr;
align-items: center;
padding: var(--padding);
gap: var(--padding);
fill: var(--color-accent);
font-size: 13px;
color: rgba(255, 255, 255, .5);
cursor: pointer;
}
searchbox > svg {
width: var(--icon-size);
}
/* ### Dialog */
body.search-dialog-open main {
transform: scale(.94);
}
dialog.search {
transition: 200ms height cubic-bezier(.41,0,.34,.99);
margin: auto;
width: 100%;
max-width: 1000px;
height: calc(var(--running-size) + (var(--padding) * 5));
max-height: 1000px;
border-color: transparent;
background-color: transparent;
overflow: visible;
outline: none;
}
dialog.search.active {
height: 70vh;
}
dialog.search search {
transition: 400ms transform, 200ms opacity;
width: 100%;
height: 100%;
display: grid;
grid-template-rows: var(--running-size) 1fr;
gap: calc(var(--padding) * 2);
transform: scale(1.1);
overflow: hidden;
background-color: rgba(255, 255, 255, .05);
-webkit-backdrop-filter: blur(20px);
backdrop-filter: brightness(.3) blur(20px);
border-radius: 12px;
box-shadow: 0 10px 30px 10px black;
opacity: 0;
}
body.search-dialog-open dialog.search search {
transform: scale(1);
padding: calc(var(--padding) * 1.5);
opacity: 1;
}
search input {
transition: 200ms background-color, 200ms box-shadow, 200ms color;
border-radius: 6px;
border: none;
outline: none;
color: black;
font-size: 16px;
padding: var(--padding) calc(var(--padding) * 1.5);
background-color: rgba(255, 255, 255, .05);
box-shadow: 0 5px 70px 10px rgba(0, 0, 0, .3);
color: white;
}
search input:focus {
background-color: rgba(255, 255, 255, .9);
box-shadow: 0 10px 30px 10px black;
color: black;
}
/* ### Search results */
dialog.search search search-results {
overflow-y: auto;
}
dialog.search search search-results > svg {
margin: auto;
width: 150px;
fill: rgba(255, 255, 255, .05);
}
/* # Feature queries */
@media (hover: hover) {
:is(h1, h2, h3, p, li) > a:hover {
text-underline-offset: 1px;
text-decoration-thickness: calc(var(--underline-tickness) * 2);
color: var(--color-accent);
}
/* # Components */
button {
transition: 200ms background-color, 200ms border-color, 200ms color;
}
button:hover {
border-color: rgba(255, 255, 255, .2);
background-color: rgba(255, 255, 255, .1);
}
button.solid:hover {
color: var(--color-accent);
border-color: rgba(var(--primer-color-accent), .2);
background-color: rgba(var(--primer-color-accent), .2);
box-shadow: 0 -10px 20px 10px rgba(var(--primer-color-accent), .05);
}
/* ## Header */
header .logo:hover path.solid {
fill: var(--color-accent);
}
searchbox {
transition: 200ms background-color;
}
searchbox:hover {
background-color: rgba(255, 255, 255, .07);
}
}
/* # Size queries */
@media (min-width: 700px) {
header {
grid-template-columns: 1fr 250px var(--running-size);
}
header nav {
justify-self: start;
margin: 0 calc(var(--padding) / 2);
}
/* # Menu */
/* < Move the search box to the header */
header searchbox {
display: grid;
}
}

16
assets/css/fonts.css Executable file
View file

@ -0,0 +1,16 @@
@font-face {
font-family: "Roboto Mono";
ascent-override: 100%;
font-weight: 400;
size-adjust: 105%;
font-stretch: 97.5% 112.5%;
src: local("Roboto Mono Regular"), url("/assets/fonts/roboto-mono-regular.woff2") format("woff2");
}
@font-face {
font-family: "Roboto Mono";
ascent-override: 100%;
size-adjust: 95%;
font-weight: 800;
src: local("Roboto Mono Bold"), url("/assets/fonts/roboto-mono-bold.woff2") format("woff2");
}

View file

@ -5,7 +5,7 @@
--color-accent: rgb(var(--primer-color-accent));
}
vv-shell {
main {
display: flex;
flex-direction: column;
gap: var(--padding);
@ -15,7 +15,7 @@ vv-shell {
/* ## Divider */
vv-shell > hr {
main > hr {
border-color: rgba(255, 255, 255, .1);
}
@ -41,15 +41,15 @@ section.about span.interests {
animation: interests-hue 5s infinite linear;
}
section.about p i:not(:hover) {
opacity: .3;
/* ## Version */
section.version {
color: rgba(255, 255, 255, .2);
}
/* # Interests */
div.interests {
--text-shadow-blur: 30px;
transition: 300ms opacity;
position: fixed;
top: 0;
@ -58,7 +58,7 @@ div.interests {
height: 100%;
font-weight: bold;
pointer-events: none;
font-size: clamp(16px, 15vw, 50px);
font-size: 50px;
color: var(--color-accent);
overflow: hidden;
opacity: 0;
@ -70,7 +70,9 @@ div.interests.active {
}
div.interests p {
transition: 500ms transform cubic-bezier(.34,0,0,.99);
--text-shadow-blur: 30px;
transition: 300ms transform;
position: absolute;
text-shadow:
0 0 var(--text-shadow-blur) black,

View file

@ -5,33 +5,19 @@
--color-accent: rgb(var(--primer-color-accent));
}
vv-shell {
main {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--padding);
}
.fingerprint {
word-break: break-all;
}
/* # Sections */
vv-shell > svg {
main > svg {
margin: var(--padding) 0;
}
/* ## Modifiers */
section.center {
text-align: center;
}
section.fade {
opacity: .3;
}
/* ## Social */
section.social {
@ -75,14 +61,11 @@ section.social social.hovering p {
display: initial;
}
/* ## PGP key */
/* ## OpenPGP key */
section.pgp {
max-width: 800px;
position: relative;
display: flex;
flex-direction: column;
gap: var(--padding);
text-align: center;
background-color: rgba(var(--primer-color-accent), .15);
padding: calc(var(--padding) * 1.5);
@ -98,13 +81,13 @@ section.pgp > svg {
}
section.pgp > p {
padding: 0 var(--padding);
margin-bottom: var(--padding);
padding: var(--padding);
}
section.pgp .buttons {
display: flex;
flex-direction: column;
margin-top: var(--padding);
gap: var(--padding);
}
@ -175,7 +158,6 @@ section.form-message h3 {
}
section.form-message pre {
white-space: pre-wrap;
padding: var(--padding);
background-color: rgba(0, 0, 0, .15);
}
@ -189,10 +171,6 @@ section.form-message.sent {
background-color: var(--color-accent);
}
section.form-message.sent + section.form {
display: none;
}
/* # Size queries */
@media (min-width: 460px) {

View file

@ -6,7 +6,7 @@ header {
backdrop-filter: unset;
}
vv-shell {
main {
max-width: unset;
display: grid;
justify-items: center;

View file

@ -1,21 +1,15 @@
/* # Overrides */
body[vv-top-page="/"]::before {
opacity: 0;
}
/* # vv-shell styles */
/* # Main styles */
/* ## Picture */
vv-shell {
main {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: column-reverse;
}
vv-shell img {
main img {
margin: auto;
width: 25vh;
pointer-events: none;
@ -152,33 +146,25 @@ splash::after {
.menu menu li:hover {
opacity: 1;
font-weight: 100;
text-shadow: 0 0 10px rgba(var(--primer-color-accent), .4);
}
button.email:hover {
background-color: transparent;
}
/* enable font-weight hover animation */
@media not (prefers-reduced-motion: reduce) {
.menu menu li {
transition: 200ms opacity, 200ms color, 300ms font-weight;
}
}
}
/* # Size quries */
@media (min-width: 900px) {
vv-shell {
main {
display: grid;
grid-template-columns: repeat(2, 1fr);
justify-items: center;
align-items: center;
}
vv-shell img {
main img {
width: 35vh;
}
button:hover {
background-color: transparent;
}
}

View file

@ -12,7 +12,7 @@
section.search {
width: 100%;
display: none;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--padding);
@ -21,10 +21,6 @@ section.search {
margin-bottom: calc(var(--padding) * 2);
}
vv-shell[vv-page="/search"] > section.search {
display: flex;
}
section.search form {
display: contents;
}
@ -35,11 +31,6 @@ section.search search {
section.search input {
width: 100%;
border: none;
color: black;
outline: none;
padding: var(--padding);
background-color: var(--color-accent);
}
section.search button[type="submit"] {
@ -51,6 +42,10 @@ section.search > svg {
width: 100%;
}
body:not([vv-page="/search"]) section.search {
display: none;
}
/* # Search results */
section.results .result {
@ -59,6 +54,19 @@ section.results .result {
gap: calc(var(--padding) / 2);
}
/* ---- */
main > svg,
dialog.search search search-results > svg {
margin: auto;
width: 150px;
fill: rgba(255, 255, 255, .05);
}
dialog.search search search-results .empty {
text-align: center;
}
/* ## Titles */
section.title a h2 {

View file

@ -5,7 +5,7 @@
--color-accent: rgb(var(--primer-color-accent));
}
vv-shell {
main {
display: flex;
flex-direction: column;
gap: var(--padding);
@ -28,7 +28,6 @@ section.git {
}
section.git svg {
fill: white;
width: 60px;
}

Binary file not shown.

Binary file not shown.

72
assets/js/document.js Executable file
View file

@ -0,0 +1,72 @@
new vv.Interactions("document");
const mainElement = document.querySelector(vv._env.MAIN);
// Crossfade pages on navigation
// Or maybe I shouldn't... hmmm
mainElement.addEventListener(vv.Navigation.events.LOADING, () => {
mainElement.classList.add("loading");
// Clean up modified transform-origin if set after search dialog animation
mainElement.style.removeProperty("transform-origin");
});
mainElement.addEventListener(vv.Navigation.events.LOADED, () => {
[...document.querySelectorAll("dialog")].forEach(element => element.close())
// Wait 200ms for the page fade-in animation to finish
setTimeout(() => mainElement.classList.remove("loading"), 200);
});
// Search dialog open/close logic
{
const CLASNAME_DIALOG_OPEN = "search-dialog-open";
// Offset in pixels from scroll position when scaling the main element
const TRANSFORM_ORIGIN_Y_PADDING = 350;
const dialog = document.querySelector("dialog.search");
// "Polyfill" for HTMLDialogELement open and close events
(new MutationObserver((mutations) => {
// There is only one search dialog elemenet
const target = mutations[0].target;
// Set or unset dialog open class on body depending on dialog visibility
target.hasAttribute("open")
? target.dispatchEvent(new Event("open"))
: target.dispatchEvent(new Event("close"));
}).observe(dialog, { attributes: true }));
dialog.addEventListener("open", () => {
// Scale main element from the current scroll position
mainElement.style.setProperty("transform-origin", `50% calc(${window.scrollY}px + ${TRANSFORM_ORIGIN_Y_PADDING}px)`);
document.body.classList.add(CLASNAME_DIALOG_OPEN);
});
dialog.addEventListener("close", () => document.body.classList.remove(CLASNAME_DIALOG_OPEN));
// Close search dialog if dialog is clicked outside inner content
dialog.addEventListener("click", (event) => event.target === dialog ? dialog.close() : null);
// Open search dialog when searchbox is clicked
document.querySelector("searchbox").addEventListener("click", () => dialog.showModal());
}
// Search logic
{
const searchResultsElement = document.querySelector("search-results");
const search = (query) => {
new vv.Navigation(`/search?q=${query}`, {
carrySearchParams: true
}).navigate(searchResultsElement);
};
// Run search on keyup
document.querySelector("search input").addEventListener("keyup", (event) => search(event.target.value));
// Trigger expand search box animation
document.querySelector("search input").addEventListener("keydown", () => {
searchResultsElement.closest("dialog").classList.add("active");
}, { once: true });
}

View file

@ -1,38 +1,31 @@
new vv.Interactions("about");
const randomIntFromInterval = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
}
// Interest explosion effect from origin position
const explodeInterests = (originX, originY) => {
// Elements can not translate more than negative- and positive from this number
const TRANS_LIMIT = 300;
const wrapper = document.querySelector("div.interests");
wrapper.classList.add("active");
// Elements can not expand further than positive or negative of these values
const transLimitX = window.innerWidth / 4;
const transLimitY = window.innerHeight / 3;
[...wrapper.querySelectorAll("p")].forEach(element => {
const size = element.getBoundingClientRect();
// Generate random HUE wheel rotation degrees
/*
Generate random visuals for current element
*/
const hue = randomIntFromInterval(0, 360);
// Generate random element transform rotation
const rotate = randomIntFromInterval(-5, 5);
// Generate random offsets in each direction clamped to translation limit
let transX = randomIntFromInterval(transLimitX * -1, transLimitX);
let transY = randomIntFromInterval(transLimitY * -1, transLimitY);
// Clamp translation to screen left and right X size
transX = Math.max(0 - originX, Math.min((window.innerWidth - originX) - size.width, transX));
// Clamp translation to top and bottom Y size
transY = Math.max(0 - originY, Math.min((window.innerHeight - originY) - size.height, transY));
const transX = randomIntFromInterval(TRANS_LIMIT * -1, TRANS_LIMIT);
const transY = randomIntFromInterval(TRANS_LIMIT * -1, TRANS_LIMIT);
// Set initial position
element.style.setProperty("top", `${originY}px`);
element.style.setProperty("left", `${originX}px`);
// Set HUE rotation
// Set random HUE rotation
element.style.setProperty("-webkit-filter", `hue-rotate(${hue}deg)`);
element.style.setProperty("filter", `hue-rotate(${hue}deg)`);
@ -46,8 +39,10 @@ const implodeInterests = () => {
const wrapper = document.querySelector("div.interests");
wrapper.classList.remove("active");
// Reset to initial position
[...wrapper.querySelectorAll("p")].forEach(element => element.style.setProperty("transform", "translate(0, 0)"));
[...wrapper.querySelectorAll("p")].forEach(element => {
// Reset to initial position
element.style.setProperty("transform", "translate(0, 0)");
});
};
// Bind triggers for interests explosion and implotion
@ -60,7 +55,10 @@ const implodeInterests = () => {
// Get absolute position of the trigger element
const size = interestsElement.getBoundingClientRect();
explodeInterests(size.x, size.y);
const x = size.x - 80;
const y = size.y - 10;
explodeInterests(x, y);
});
interestsElement.addEventListener(canHover ? "mouseleave" : "touchend", () => implodeInterests());

View file

@ -10,6 +10,8 @@ class ContactForm {
[...document.querySelectorAll("form :is(input, textarea)")].forEach(element => {
element.addEventListener("keyup", () => this.saveMessage());
});
}
// Get saved message as JSON from SessionStorage
@ -34,7 +36,6 @@ class ContactForm {
return ContactForm.removeSavedMessage();
}
// Set value of each input field in DOM by name attribute
for (const [name, value] of Object.entries(message)) {
this.form.querySelector(`[name="${name}"]`).value = value;
}

View file

120
assets/js/pages/index.js Executable file
View file

@ -0,0 +1,120 @@
const EMAIL_CPY_ANIM_DUR_MSECONDS = 1000;
// Run email copied splash animation
const emailCopiedAnimation = () => {
const CONFETTI_COUNT = 40;
const CONFETTI_SCALE_PIXELS = 300;
const randomIntFromInterval = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min)
}
// Create new splash element
const splashElement = document.createElement("splash");
splashElement.innerText = "copied!";
// Set inline display to none to hide this element on pages where the splash element has no override styles defined.
splashElement.style.display = "none";
// Array of box-shadow strings as "confetti"
const confetti = [];
// Generate random confetti
for (let i = 0; i < CONFETTI_COUNT; i++) {
// Random confetti position
const x = randomIntFromInterval(CONFETTI_SCALE_PIXELS * -1, CONFETTI_SCALE_PIXELS);
const y = randomIntFromInterval(CONFETTI_SCALE_PIXELS * -1, CONFETTI_SCALE_PIXELS);
// Random confetti RGB color
const rgb = [
randomIntFromInterval(0, 255),
randomIntFromInterval(0, 255),
randomIntFromInterval(0, 255)
];
// Interpolate random values and append to outer confetti array
confetti.push(`${x}px ${y}px 0 rgb(${rgb.join(",")})`);
}
// Set CSS variable on splash element that in turn will be used by pseudo-element
splashElement.style.setProperty("--confetti", confetti.join(","));
// Start animation by appending the created element to the document body
document.body.appendChild(splashElement);
// Run hide animation
setTimeout(() => {
splashElement.classList.add("hide");
// Selfdestruct element when hide animation finishes
setTimeout(() => splashElement.remove(), 400);
}, EMAIL_CPY_ANIM_DUR_MSECONDS + 100);
}
new vv.Interactions("index", {
// Copy email address to clipboard
copyEmail: async () => {
try {
await navigator.clipboard.writeText("victor@vlw.se");
// Run "email copied" animation!
emailCopiedAnimation();
// NOTE: I don't know, spamming the button is kinda fun
// Prevent interactions with the copy email elements while the animation is running
/*[...document.querySelectorAll("[vv-call='copyEmail']")].forEach(element => {
//element.classList.add("lock");
setTimeout(() => element.classList.remove("lock"), EMAIL_CPY_ANIM_DUR_MSECONDS);
});*/
} catch (error) {
console.error(error.message);
}
},
// Open the fullscreen menu
openMenu: () => document.querySelector("menu").classList.add("active"),
// Close the fullscreen menu
closeMenu: () => document.querySelector("menu").classList.remove("active")
});
// Change site accent color on hover of menu items
if (window.matchMedia("(hover: hover)")) {
// Update root CSS variables
const updateColor = (rgb = null, hue = 0) => {
if (!rgb) {
document.documentElement.style.removeProperty("--hue-accent");
document.documentElement.style.removeProperty("--primer-color-accent");
document.documentElement.style.removeProperty("--color-accent");
return;
}
document.documentElement.style.setProperty("--hue-accent", `${hue}deg`);
document.documentElement.style.setProperty("--primer-color-accent", `${rgb}`);
// Compiled color variable must to be updated to receive the new RGB values
document.documentElement.style.setProperty("--color-accent", "rgb(var(--primer-color-accent)");
};
[...document.querySelectorAll("menu li")].forEach(element => {
// Change site accent color to RGB and HUE rotation defined in element dataset
element.addEventListener("mouseenter", (event) => updateColor(event.target.dataset.rgb, event.target.dataset.hue));
// Reset initial accent color and hues
element.addEventListener("mouseleave", () => updateColor());
});
// Reset color on navigation
document.querySelector(vv._env.MAIN).addEventListener(vv.Navigation.events.LOADED, () => updateColor(), { once: true });
}
// Open search box from mobile fullscreen menu
{
// Open search dialog when searchbox is clicked
document.querySelector("menu searchbox").addEventListener("click", () => {
// Search box dialog element
document.querySelector("dialog.search").showModal();
// Close fullscreen menu
document.querySelector("menu").classList.remove("active");
});
}

25
assets/js/pages/search.js Executable file
View file

@ -0,0 +1,25 @@
// Don't open the search dialog overlay if search page is open stand-alone
{
const searchBox = document.querySelector("body:not(.search-dialog-open) searchbox");
// Page is stand-alone
if (searchBox) {
// Shift focus to the on-page search box instead of opening search dialog on click
const shiftSearchboxFocus = () => {
// Override normal "open search dialog" behavior
document.querySelector("dialog.search").close();
// Shift focus to the on-page search input instead
}
// Bind event listener to searchbox element
document.querySelector("body:not(.search-dialog-open) searchbox").addEventListener("click", shiftSearchboxFocus, true);
// Remove event listener from searchbox element on page navigation
mainElement.addEventListener(vv.Navigation.events.LOADING, () => {
searchBox.removeEventListener("click", shiftSearchboxFocus);
});
}
}
new vv.Interactions("search");

1
assets/js/pages/work.js Executable file
View file

@ -0,0 +1 @@
new vv.Interactions("work");

View file

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View file

View file

View file

View file

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

1
assets/media/icons/github.svg Executable file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98 98"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 957 B

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

View file

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Some files were not shown because too many files have changed in this diff Show more