Compare commits
40 commits
fa1c889f1d
...
474ea9b4ae
Author | SHA1 | Date | |
---|---|---|---|
474ea9b4ae | |||
1ac2704124 | |||
f551d5d889 | |||
51e8215e78 | |||
7f4b54685e | |||
6dad22f226 | |||
0483e092dd | |||
ae1e992c5f | |||
43ddf1fdf6 | |||
903a14b3ae | |||
99e9996e93 | |||
7cd41d3e13 | |||
bef1bab522 | |||
285bdc3980 | |||
ce1f3e3bab | |||
32f13b356a | |||
608f775f24 | |||
e1e4c3fd1a | |||
0478685791 | |||
fd04c3d5ae | |||
9b3ab0b17b | |||
a7655f9cdb | |||
412a457bee | |||
982acc6a40 | |||
3fd7ce6bf0 | |||
e1e3a4a68a | |||
87cf63c884 | |||
3154afab3c | |||
f963a8993d | |||
3f944e1a33 | |||
94d20d51ae | |||
a7d2730aac | |||
8dccd20d39 | |||
a82c853153 | |||
6ef024db6f | |||
a8a1c5791f | |||
397484c5b4 | |||
efa84d9e6c | |||
8c815956e2 | |||
150559b075 |
|
@ -2,3 +2,6 @@
|
|||
base_url = "https://api.vlw.one/"
|
||||
api_key = ""
|
||||
verify_peer = 0
|
||||
|
||||
[time]
|
||||
date_time_zone = "Europe/Stockholm"
|
8
.gitignore
vendored
|
@ -1,8 +1,14 @@
|
|||
assets/media/content
|
||||
# Public assets #
|
||||
#################
|
||||
public/robots.txt
|
||||
public/.well-known
|
||||
|
||||
assets/js/modules/npm
|
||||
|
||||
# Bootstrapping #
|
||||
#################
|
||||
vendor
|
||||
node_modules
|
||||
.env.ini
|
||||
|
||||
# OS generated files #
|
||||
|
|
34
README.md
|
@ -1,27 +1,38 @@
|
|||
# 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).
|
||||
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).
|
||||
|
||||
# Installation
|
||||
If you for whatever reason want to get this website up and running for yourself this is how that is done.
|
||||
|
||||
This website is built for PHP 8.0+ and MariaDB 14+ (for the API database).
|
||||
## 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/)
|
||||
|
||||
**Confimed supported framework versions:**
|
||||
Vegvisir|Reflect
|
||||
--|--
|
||||
✅ [`3.0.1`](https://codeberg.org/vegvisir/vegvisir/releases/tag/3.0.1)|✅ [`2.7.2`](https://codeberg.org/reflect/reflect/releases/tag/2.7.2)
|
||||
|
||||
## Website (Vegvisir)
|
||||
1. **Download this repo**
|
||||
|
||||
Git clone or download this repo to any local folder
|
||||
```
|
||||
git clone https://github.com/VictorWesterlund/vlw.se
|
||||
git clone https://codeberg.org/vlw/vlw.se
|
||||
```
|
||||
2. **Download and install Vegvisir**
|
||||
|
||||
Follow the installation instructions for [Vegvisir](https://github.com/victorwesterlund/vegvisir) and point the `site_path` variable to the local vlw.se folder.
|
||||
Follow the installation instructions for [Vegvisir](https://vegvisir.vlw.se/docs/installation) and point the `root_path` variable to your local vlw.se folder.
|
||||
|
||||
3. **Install dependencies**
|
||||
3. **Run the install script**
|
||||
|
||||
Install dependencies with composer.
|
||||
This bash script will install dependencies and make npm modules public.
|
||||
```
|
||||
composer install --optimize-autoloader
|
||||
./install.sh
|
||||
```
|
||||
|
||||
Et voila! You probably want to install the API-side too but the website itself should now be accessible from your configured Vegvisir host.
|
||||
|
@ -35,16 +46,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://github.com/VictorWesterlund/vlw.se
|
||||
git clone https://codeberg.org/vlw/vlw.se
|
||||
```
|
||||
|
||||
2. **Download and install Reflect**
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
3. **Install dependencies**
|
||||
|
||||
Install dependencies with composer.
|
||||
`cd` into the api folder and install dependencies with composer.
|
||||
```
|
||||
composer install --optimize-autoloader
|
||||
```
|
||||
|
@ -57,9 +68,6 @@ 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.
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
[vlwdb]
|
||||
mariadb_host = ""
|
||||
mariadb_user = ""
|
||||
mariadb_pass = ""
|
||||
mariadb_db = ""
|
||||
[connect]
|
||||
host = ""
|
||||
user = ""
|
||||
pass = ""
|
||||
|
||||
[github]
|
||||
api_key = ""
|
||||
# Use-Agent string sent to GitHub API
|
||||
# They recommend setting it to your GitHub username or app name
|
||||
user_agent = ""
|
||||
[databases]
|
||||
vlw = ""
|
||||
battlestation = ""
|
|
@ -1,14 +1,8 @@
|
|||
{
|
||||
"require": {
|
||||
"local/api.endpoints": "1.0.0-dev",
|
||||
"reflect/plugin-rules": "^1.5",
|
||||
"victorwesterlund/xenum": "^1.1"
|
||||
"victorwesterlund/xenum": "dev-master",
|
||||
"vlw/mysql": "dev-master"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"repositories": [
|
||||
{
|
||||
"type": "path",
|
||||
"url": "src/packages/Endpoints"
|
||||
}
|
||||
]
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
|
|
61
api/composer.lock
generated
|
@ -4,33 +4,8 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "9da96ba90ef20d885034442b30dce0a3",
|
||||
"content-hash": "f3f2b3cb3bd789eee6af4a93f4a6e0f9",
|
||||
"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.0",
|
||||
|
@ -70,7 +45,7 @@
|
|||
},
|
||||
{
|
||||
"name": "victorwesterlund/xenum",
|
||||
"version": "1.1.1",
|
||||
"version": "dev-master",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/VictorWesterlund/php-xenum.git",
|
||||
|
@ -82,6 +57,7 @@
|
|||
"reference": "8972f06f42abd1f382807a67e937d5564bb89699",
|
||||
"shasum": ""
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
@ -104,13 +80,42 @@
|
|||
"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": {
|
||||
"local/api.endpoints": 20
|
||||
"victorwesterlund/xenum": 20,
|
||||
"vlw/mysql": 20
|
||||
},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
|
|
63
api/endpoints/battlestation/GET.php
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
103
api/endpoints/battlestation/chassis/GET.php
Normal file
|
@ -0,0 +1,103 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
108
api/endpoints/battlestation/coolers/GET.php
Normal file
|
@ -0,0 +1,108 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
117
api/endpoints/battlestation/cpu/GET.php
Normal file
|
@ -0,0 +1,117 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
115
api/endpoints/battlestation/dram/GET.php
Normal file
|
@ -0,0 +1,115 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
107
api/endpoints/battlestation/gpu/GET.php
Normal file
|
@ -0,0 +1,107 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
194
api/endpoints/battlestation/mb/GET.php
Normal file
|
@ -0,0 +1,194 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
102
api/endpoints/battlestation/psu/GET.php
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
110
api/endpoints/battlestation/storage/GET.php
Normal file
|
@ -0,0 +1,110 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
<?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)
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -6,20 +6,19 @@
|
|||
use ReflectRules\Rules;
|
||||
use ReflectRules\Ruleset;
|
||||
|
||||
use Reflect\Method;
|
||||
use function Reflect\Call;
|
||||
|
||||
use VLW\API\Databases\VLWdb\VLWdb;
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
use VLW\API\Databases\VLWdb\Models\Messages\MessagesModel;
|
||||
|
||||
require_once Path::root("src/databases/VLWdb.php");
|
||||
require_once Path::root("src/databases/models/Messages.php");
|
||||
require_once Path::root("src/databases/models/Messages/Messages.php");
|
||||
|
||||
class POST_Messages extends VLWdb {
|
||||
protected Ruleset $ruleset;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->ruleset = new Ruleset(strict: true);
|
||||
|
||||
$this->ruleset->POST([
|
||||
|
@ -34,46 +33,19 @@
|
|||
->min(1)
|
||||
->max(parent::MYSQL_TEXT_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);
|
||||
parent::__construct(Databases::VLW, $this->ruleset);
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
//return new Response(["hello" => "maybe"], 500);
|
||||
// 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();
|
||||
}
|
||||
$entity[MessagesModel::ID->value] = parent::gen_uuid4();
|
||||
$entity[MessagesModel::DATE_CREATED->value] = time();
|
||||
|
||||
// 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);
|
||||
return $this->db->for(MessagesModel::TABLE)->insert($entity) === true
|
||||
? new Response($entity[MessagesModel::ID->value], 201)
|
||||
: new Response("Failed to create message", 500);
|
||||
}
|
||||
}
|
|
@ -1,223 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -9,13 +9,15 @@
|
|||
|
||||
use VLW\API\Endpoints;
|
||||
|
||||
use VLW\API\Databases\VLWdb\VLWdb;
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
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.php");
|
||||
require_once Path::root("src/databases/models/WorkActions.php");
|
||||
require_once Path::root("src/databases/models/Work/Work.php");
|
||||
|
||||
class GET_Search extends VLWdb {
|
||||
const GET_QUERY = "q";
|
||||
|
@ -23,118 +25,35 @@
|
|||
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(2)
|
||||
->min(1)
|
||||
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
|
||||
]);
|
||||
|
||||
parent::__construct(Databases::VLW, $this->ruleset);
|
||||
}
|
||||
|
||||
// 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);
|
||||
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();
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
// 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()
|
||||
$results = [
|
||||
Endpoints::WORK->value => $this->search_work()->output()
|
||||
];
|
||||
|
||||
// Count total number of results from all categories
|
||||
$total_num_results = 0;
|
||||
foreach (array_values($categories) as $results) {
|
||||
$total_num_results += count($results);
|
||||
}
|
||||
// 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)));
|
||||
|
||||
return new Response([
|
||||
"query" => $_GET[self::GET_QUERY],
|
||||
"results" => $categories,
|
||||
"total_num_results" => $total_num_results
|
||||
]);
|
||||
// Return 404 if no search results
|
||||
return new Response($results, $num_results > 0 ? 200 : 404);
|
||||
}
|
||||
}
|
|
@ -1,15 +1,19 @@
|
|||
<?php
|
||||
|
||||
|
||||
use Reflect\Path;
|
||||
use Reflect\Response;
|
||||
use ReflectRules\Type;
|
||||
use ReflectRules\Rules;
|
||||
use ReflectRules\Ruleset;
|
||||
|
||||
use VLW\API\Databases\VLWdb\VLWdb;
|
||||
use const VLW\API\RESP_DELETE_OK;
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
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");
|
||||
|
||||
|
@ -17,44 +21,47 @@
|
|||
protected Ruleset $ruleset;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->ruleset = new Ruleset(strict: true);
|
||||
|
||||
$this->ruleset->GET([
|
||||
(new Rules("id"))
|
||||
->required()
|
||||
$this->ruleset->POST([
|
||||
(new Rules(WorkModel::ID->value))
|
||||
->type(Type::STRING)
|
||||
->min(1)
|
||||
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
|
||||
->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)
|
||||
]);
|
||||
}
|
||||
|
||||
// # 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);
|
||||
parent::__construct(Databases::VLW, $this->ruleset);
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
// 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();
|
||||
return $this->db->for(FieldsEnumsModel::TABLE)->delete($_POST) === true
|
||||
? new Response(RESP_DELETE_OK)
|
||||
: new Response("Failed to delete work entity", 500);
|
||||
}
|
||||
}
|
|
@ -1,136 +1,98 @@
|
|||
<?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\Endpoints;
|
||||
|
||||
use VLW\API\Databases\VLWdb\VLWdb;
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
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.php");
|
||||
require_once Path::root("src/databases/models/WorkTags.php");
|
||||
require_once Path::root("src/databases/models/WorkActions.php");
|
||||
require_once Path::root("src/databases/models/Work/Work.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("id"))
|
||||
(new Rules(WorkModel::ID->value))
|
||||
->type(Type::STRING)
|
||||
->min(1)
|
||||
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
|
||||
->default(null)
|
||||
]);
|
||||
}
|
||||
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
|
||||
|
||||
// # Helper methods
|
||||
(new Rules(WorkModel::TITLE->value))
|
||||
->type(Type::STRING)
|
||||
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
|
||||
|
||||
private function fetch_row_tags(string $id): array {
|
||||
$resp = $this->db->for(WorkTagsModel::TABLE)
|
||||
->where([WorkTagsModel::ANCHOR->value => $id])
|
||||
->select(WorkTagsModel::NAME->value);
|
||||
(new Rules(WorkModel::SUMMARY->value))
|
||||
->type(Type::STRING)
|
||||
->max(parent::MYSQL_TEXT_MAX_LENGTH),
|
||||
|
||||
return parent::is_mysqli_result($resp) ? $resp->fetch_all(MYSQLI_ASSOC) : [];
|
||||
}
|
||||
(new Rules(WorkModel::IS_LISTABLE->value))
|
||||
->type(Type::BOOLEAN)
|
||||
->default(true),
|
||||
|
||||
// # Responses
|
||||
(new Rules(WorkModel::IS_READABLE->value))
|
||||
->type(Type::BOOLEAN)
|
||||
->default(true),
|
||||
|
||||
// Return 422 Unprocessable Content error if request validation failed
|
||||
private function resp_rules_invalid(): Response {
|
||||
return new Response($this->ruleset->get_errors(), 422);
|
||||
}
|
||||
(new Rules(WorkModel::DATE_MODIFIED->value))
|
||||
->type(Type::NUMBER)
|
||||
->min(1)
|
||||
->max(parent::MYSQL_INT_MAX_LENGTH),
|
||||
|
||||
// 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::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
|
||||
(new Rules(WorkModel::DATE_CREATED->value))
|
||||
->type(Type::NUMBER)
|
||||
->min(1)
|
||||
->max(parent::MYSQL_INT_MAX_LENGTH)
|
||||
]);
|
||||
|
||||
// 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);
|
||||
parent::__construct(Databases::VLW, $this->ruleset);
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
// Bail out if request validation failed
|
||||
if (!$this->ruleset->is_valid()) {
|
||||
return $this->resp_rules_invalid();
|
||||
// Use copy of search paramters as filters
|
||||
$filters = $_GET;
|
||||
|
||||
// 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]}%"
|
||||
];
|
||||
}
|
||||
|
||||
// Return details about a specific item by id
|
||||
if (!empty($_GET["id"])) {
|
||||
return $this->resp_item_details($_GET["id"]);
|
||||
// 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]}%"
|
||||
];
|
||||
}
|
||||
|
||||
$resp = $this->db->for(WorkModel::TABLE)
|
||||
->where([WorkModel::IS_LISTABLE->value => true])
|
||||
->order([WorkModel::DATE_TIMESTAMP_CREATED->value => "DESC"])
|
||||
$response = $this->db->for(WorkModel::TABLE)
|
||||
->where($filters)
|
||||
->order([WorkModel::DATE_CREATED->value => "DESC"])
|
||||
->select([
|
||||
WorkModel::ID->value,
|
||||
WorkModel::TITLE->value,
|
||||
WorkModel::SUMMARY->value,
|
||||
WorkModel::COVER_SRCSET->value,
|
||||
WorkModel::IS_LISTABLE->value,
|
||||
WorkModel::IS_READABLE->value,
|
||||
WorkModel::DATE_YEAR->value,
|
||||
WorkModel::DATE_MONTH->value,
|
||||
WorkModel::DATE_DAY->value,
|
||||
WorkModel::DATE_TIMESTAMP_MODIFIED->value,
|
||||
WorkModel::DATE_TIMESTAMP_CREATED->value
|
||||
WorkModel::DATE_MODIFIED->value,
|
||||
WorkModel::DATE_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);
|
||||
return $response->num_rows > 0
|
||||
? new Response($response->fetch_all(MYSQLI_ASSOC))
|
||||
: new Response([], 404);
|
||||
}
|
||||
}
|
|
@ -1,30 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Reflect\Call;
|
||||
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\Work\WorkModel;
|
||||
use VLW\API\Databases\VLWdb\Models\WorkPermalinks\WorkPermalinksModel;
|
||||
use VLW\API\Endpoints;
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
use VLW\API\Databases\VLWdb\Models\Work\{
|
||||
WorkModel,
|
||||
WorkPermalinksModel
|
||||
};
|
||||
|
||||
require_once Path::root("src/Endpoints.php");
|
||||
require_once Path::root("src/databases/VLWdb.php");
|
||||
require_once Path::root("src/databases/models/Work.php");
|
||||
require_once Path::root("src/databases/models/WorkPermalinks.php");
|
||||
require_once Path::root("src/databases/models/Work/Work.php");
|
||||
require_once Path::root("src/databases/models/Work/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([
|
||||
|
@ -52,19 +53,19 @@
|
|||
(new Rules(WorkModel::IS_READABLE->value))
|
||||
->type(Type::BOOLEAN),
|
||||
|
||||
(new Rules(WorkModel::DATE_TIMESTAMP_CREATED->value))
|
||||
(new Rules(WorkModel::DATE_MODIFIED->value))
|
||||
->type(Type::NUMBER)
|
||||
->min(0)
|
||||
->max(parent::MYSQL_INT_MAX_LENGHT)
|
||||
->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)
|
||||
]);
|
||||
|
||||
$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();
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
// Generate a slug URL from string
|
||||
|
@ -72,130 +73,46 @@
|
|||
return strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $input)));
|
||||
}
|
||||
|
||||
// # 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();
|
||||
// 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])
|
||||
];
|
||||
}
|
||||
|
||||
// Return 402 Conflict
|
||||
return new Response("No entity with id '{$_GET["id"]}' was found", 404);
|
||||
}
|
||||
|
||||
return $this->current_entity;
|
||||
}
|
||||
|
||||
// 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);
|
||||
private function get_entity_by_id(string $id): Response {
|
||||
return (new Call(Endpoints::WORK->value))->params([
|
||||
WorkModel::ID->value => $id
|
||||
])->get();
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
// Bail out if request validation failed
|
||||
if (!$this->ruleset->is_valid()) {
|
||||
return $this->resp_rules_invalid();
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Empty payload, nothing to do
|
||||
if (empty($_POST)) {
|
||||
return $this->resp_no_changes();
|
||||
// Add the new slug to update entity
|
||||
$entity[WorkModel::ID] = $slug;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
// Generate new work date fields from timestamp
|
||||
if ($_POST[WorkModel::DATE_CREATED->value]) {
|
||||
array_merge($entity, self::gen_date_created());
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Return 200 OK and new or existing entity slug as body
|
||||
return new Response($this->current_entity->output()[WorkModel::ID->value]);
|
||||
// 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);
|
||||
}
|
||||
}
|
|
@ -1,27 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Reflect\Call;
|
||||
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\Work\WorkModel;
|
||||
use VLW\API\Databases\VLWdb\Models\WorkPermalinks\WorkPermalinksModel;
|
||||
use VLW\API\Endpoints;
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
use VLW\API\Databases\VLWdb\Models\Work\{
|
||||
WorkModel,
|
||||
WorkPermalinksModel
|
||||
};
|
||||
|
||||
require_once Path::root("src/Endpoints.php");
|
||||
require_once Path::root("src/databases/VLWdb.php");
|
||||
require_once Path::root("src/databases/models/Work.php");
|
||||
require_once Path::root("src/databases/models/WorkPermalinks.php");
|
||||
require_once Path::root("src/databases/models/Work/Work.php");
|
||||
require_once Path::root("src/databases/models/Work/WorkPermalinks.php");
|
||||
|
||||
class POST_Work extends VLWdb {
|
||||
protected Ruleset $ruleset;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->ruleset = new Ruleset(strict: true);
|
||||
|
||||
$this->ruleset->POST([
|
||||
|
@ -37,12 +41,22 @@
|
|||
->max(parent::MYSQL_TEXT_MAX_LENGTH)
|
||||
->default(null),
|
||||
|
||||
(new Rules(WorkModel::DATE_TIMESTAMP_CREATED->value))
|
||||
(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))
|
||||
->type(Type::NUMBER)
|
||||
->min(1)
|
||||
->max(parent::MYSQL_INT_MAX_LENGHT)
|
||||
->default(null)
|
||||
->max(parent::MYSQL_INT_MAX_LENGTH)
|
||||
->default(time())
|
||||
]);
|
||||
|
||||
parent::__construct(Databases::VLW, $this->ruleset);
|
||||
}
|
||||
|
||||
// Generate a slug URL from string
|
||||
|
@ -50,84 +64,51 @@
|
|||
return strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $input)));
|
||||
}
|
||||
|
||||
// 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
|
||||
]);
|
||||
// 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];
|
||||
|
||||
return $create->ok;
|
||||
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)
|
||||
];
|
||||
}
|
||||
|
||||
// # 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 get_entity_by_id(string $id): Response {
|
||||
return (new Call(Endpoints::WORK->value))->params([
|
||||
WorkModel::ID->value => $id
|
||||
])->get();
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
// Bail out if request validation failed
|
||||
if (!$this->ruleset->is_valid()) {
|
||||
return $this->resp_rules_invalid();
|
||||
}
|
||||
// Use copy of request body as entity
|
||||
$entity = $_POST;
|
||||
|
||||
// Generate URL slug from title text or UUID if undefined
|
||||
$slug = !empty($_POST["title"]) ? self::gen_slug($_POST["title"]) : parent::gen_uuid4();
|
||||
$entity[WorkModel::ID->value] = $_POST[WorkModel::TITLE->value]
|
||||
? self::gen_slug($_POST[WorkModel::TITLE->value])
|
||||
: parent::gen_uuid4();
|
||||
|
||||
// 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();
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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());
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
// 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,
|
||||
// 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()
|
||||
]);
|
||||
|
||||
// Bail out if insert failed
|
||||
if (!$insert) {
|
||||
return $this->resp_database_error();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
|
@ -6,10 +6,11 @@
|
|||
use ReflectRules\Rules;
|
||||
use ReflectRules\Ruleset;
|
||||
|
||||
use Reflect\Method;
|
||||
use function Reflect\Call;
|
||||
|
||||
use VLW\API\Databases\VLWdb\VLWdb;
|
||||
use const VLW\API\RESP_DELETE_OK;
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel;
|
||||
|
||||
require_once Path::root("src/databases/VLWdb.php");
|
||||
|
@ -23,51 +24,15 @@
|
|||
$this->ruleset = new Ruleset(strict: true);
|
||||
|
||||
$this->ruleset->POST([
|
||||
(new Rules("id"))
|
||||
->required()
|
||||
->type(Type::STRING)
|
||||
(new Rules(WorkActionsModel::REF_WORK_ID->value))
|
||||
->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 {
|
||||
// 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();
|
||||
return $this->db->for(WorkActionsModel::TABLE)->delete($_POST) === true
|
||||
? new Response(RESP_DELETE_OK)
|
||||
: new Response("Failed to delete action for work entity", 500);
|
||||
}
|
||||
}
|
|
@ -6,62 +6,44 @@
|
|||
use ReflectRules\Rules;
|
||||
use ReflectRules\Ruleset;
|
||||
|
||||
use VLW\API\Databases\VLWdb\VLWdb;
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel;
|
||||
|
||||
require_once Path::root("src/databases/VLWdb.php");
|
||||
require_once Path::root("src/databases/models/WorkActions.php");
|
||||
require_once Path::root("src/databases/models/Work/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::ANCHOR->value))
|
||||
->required()
|
||||
(new Rules(WorkActionsModel::REF_WORK_ID->value))
|
||||
->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);
|
||||
parent::__construct(Databases::VLW, $this->ruleset);
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
// 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]])
|
||||
$response = $this->db->for(WorkActionsModel::TABLE)
|
||||
->where($_GET)
|
||||
->select([
|
||||
WorkActionsModel::REF_WORK_ID->value,
|
||||
WorkActionsModel::DISPLAY_TEXT->value,
|
||||
WorkActionsModel::HREF->value,
|
||||
WorkActionsModel::CLASS_LIST->value,
|
||||
WorkActionsModel::EXTERNAL->value
|
||||
]);
|
||||
|
||||
// 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([]);
|
||||
return $response->num_rows > 0
|
||||
? new Response($response->fetch_all(MYSQLI_ASSOC))
|
||||
: new Response([], 404);
|
||||
}
|
||||
}
|
|
@ -1,36 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Reflect\Call;
|
||||
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\Work\WorkActionsModel;
|
||||
use VLW\API\Endpoints;
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
use VLW\API\Databases\VLWdb\Models\Work\{
|
||||
WorkModel,
|
||||
WorkActionsModel
|
||||
};
|
||||
|
||||
require_once Path::root("src/Endpoints.php");
|
||||
require_once Path::root("src/databases/VLWdb.php");
|
||||
require_once Path::root("src/databases/models/WorkActions.php");
|
||||
require_once Path::root("src/databases/models/Work/Work.php");
|
||||
require_once Path::root("src/databases/models/Work/WorkActions.php");
|
||||
|
||||
class POST_WorkActions extends VLWdb {
|
||||
protected Ruleset $ruleset;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->ruleset = new Ruleset(strict: true);
|
||||
|
||||
$this->ruleset->GET([
|
||||
(new Rules("id"))
|
||||
->required()
|
||||
->type(Type::STRING)
|
||||
->min(1)
|
||||
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
|
||||
]);
|
||||
|
||||
$this->ruleset->POST([
|
||||
(new Rules(WorkActionsModel::REF_WORK_ID->value))
|
||||
->required()
|
||||
->min(1)
|
||||
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
|
||||
|
||||
(new Rules(WorkActionsModel::DISPLAY_TEXT->value))
|
||||
->required()
|
||||
->type(Type::STRING)
|
||||
|
@ -47,56 +50,31 @@
|
|||
(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);
|
||||
}
|
||||
|
||||
// # 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 static function get_entity(): Response {
|
||||
return (new Call(Endpoints::WORK->value))->params([
|
||||
WorkModel::ID->value => $_POST[WorkActionsModel::REF_WORK_ID->value]
|
||||
])->get();
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
// Bail out if request validation failed
|
||||
if (!$this->ruleset->is_valid()) {
|
||||
return $this->resp_rules_invalid();
|
||||
// Bail out if work entity could not be fetched
|
||||
$entity = self::get_entity();
|
||||
if (!$entity->ok) {
|
||||
return $entity;
|
||||
}
|
||||
|
||||
// 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();
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -1,60 +1,52 @@
|
|||
<?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\WorkPermalinks\WorkPermalinksModel;
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
use VLW\API\Databases\VLWdb\Models\Work\WorkPermalinksModel;
|
||||
|
||||
require_once Path::root("src/databases/VLWdb.php");
|
||||
require_once Path::root("src/databases/models/WorkPermalinks.php");
|
||||
require_once Path::root("src/databases/models/Work/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("id"))
|
||||
->required()
|
||||
(new Rules(WorkPermalinksModel::ID->value))
|
||||
->type(Type::STRING)
|
||||
->min(1)
|
||||
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
|
||||
|
||||
(new Rules(WorkPermalinksModel::REF_WORK_ID->value))
|
||||
->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 resolve permalink, please try again later", 503);
|
||||
parent::__construct(Databases::VLW, $this->ruleset);
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
// Bail out if request validation failed
|
||||
if (!$this->ruleset->is_valid()) {
|
||||
return $this->resp_rules_invalid();
|
||||
}
|
||||
$response = $this->db->for(WorkPermalinksModel::TABLE)
|
||||
->where($_GET)
|
||||
->select([
|
||||
WorkPermalinksModel::ID->value,
|
||||
WorkPermalinksModel::REF_WORK_ID->value,
|
||||
WorkPermalinksModel::DATE_CREATED->value
|
||||
]);
|
||||
|
||||
// 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();
|
||||
return $response->num_rows > 0
|
||||
? new Response($response->fetch_all(MYSQLI_ASSOC))
|
||||
: new Response([], 404);
|
||||
}
|
||||
}
|
|
@ -1,83 +1,65 @@
|
|||
<?php
|
||||
|
||||
|
||||
use Reflect\Call;
|
||||
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\WorkPermalinks\WorkPermalinksModel;
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
use VLW\API\Databases\VLWdb\Models\Work\WorkPermalinksModel;
|
||||
|
||||
require_once Path::root("src/databases/VLWdb.php");
|
||||
require_once Path::root("src/databases/models/WorkPermalinks.php");
|
||||
require_once Path::root("src/databases/models/Work/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("slug"))
|
||||
(new Rules(WorkPermalinksModel::ID->value))
|
||||
->required()
|
||||
->type(Type::STRING)
|
||||
->min(1)
|
||||
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
|
||||
|
||||
(new Rules("anchor"))
|
||||
(new Rules(WorkPermalinksModel::REF_WORK_ID->value))
|
||||
->required()
|
||||
->type(Type::STRING)
|
||||
->min(1)
|
||||
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
|
||||
->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())
|
||||
]);
|
||||
|
||||
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);
|
||||
private static function get_entity(): Response {
|
||||
return (new Call(Endpoints::WORK->value))->params([
|
||||
WorkModel::ID->value => $_POST[WorkTagsModel::REF_WORK_ID->value]
|
||||
])->get();
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
// Bail out if request validation failed
|
||||
if (!$this->ruleset->is_valid()) {
|
||||
return $this->resp_rules_invalid();
|
||||
// Bail out if work entity could not be fetched
|
||||
$entity = self::get_entity();
|
||||
if (!$entity->ok) {
|
||||
return $entity;
|
||||
}
|
||||
|
||||
// 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();
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -6,75 +6,37 @@
|
|||
use ReflectRules\Rules;
|
||||
use ReflectRules\Ruleset;
|
||||
|
||||
use Reflect\Method;
|
||||
use function Reflect\Call;
|
||||
|
||||
use VLW\API\Databases\VLWdb\VLWdb;
|
||||
use const VLW\API\RESP_DELETE_OK;
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
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/WorkTags.php");
|
||||
require_once Path::root("src/databases/models/Work/WorkTags.php");
|
||||
|
||||
class DELETE_WorkTags extends VLWdb {
|
||||
protected Ruleset $ruleset;
|
||||
private Ruleset $ruleset;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->ruleset = new Ruleset(strict: true);
|
||||
|
||||
$this->ruleset->POST([
|
||||
(new Rules("id"))
|
||||
->required()
|
||||
->type(Type::STRING)
|
||||
$this->ruleset->GET([
|
||||
(new Rules(WorkTagsModel::REF_WORK_ID->value))
|
||||
->min(1)
|
||||
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
|
||||
|
||||
(new Rules(WorkTagsModel::NAME->value))
|
||||
->required()
|
||||
->type(Type::ENUM, WorkTagsNameEnum::names())
|
||||
]);
|
||||
}
|
||||
|
||||
// # 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);
|
||||
parent::__construct(Databases::VLW, $this->ruleset);
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
// 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();
|
||||
return $this->db->for(WorkTagsModel::TABLE)->delete($_POST) === true
|
||||
? new Response(RESP_DELETE_OK)
|
||||
: new Response("Failed to delete value from document", 500);
|
||||
}
|
||||
}
|
51
api/endpoints/work/tags/GET.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,93 +1,63 @@
|
|||
<?php
|
||||
|
||||
use Reflect\Call;
|
||||
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\Work\WorkTagsModel;
|
||||
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsNameEnum;
|
||||
use VLW\API\Endpoints;
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
use VLW\API\Databases\VLWdb\Models\Work\{
|
||||
WorkModel,
|
||||
WorkTagsModel,
|
||||
WorkTagsNameEnum
|
||||
};
|
||||
|
||||
require_once Path::root("src/Endpoints.php");
|
||||
require_once Path::root("src/databases/VLWdb.php");
|
||||
require_once Path::root("src/databases/models/WorkTags.php");
|
||||
require_once Path::root("src/databases/models/Work/Work.php");
|
||||
require_once Path::root("src/databases/models/Work/WorkTags.php");
|
||||
|
||||
class POST_WorkTags extends VLWdb {
|
||||
protected Ruleset $ruleset;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->ruleset = new Ruleset(strict: true);
|
||||
|
||||
$this->ruleset->GET([
|
||||
(new Rules("id"))
|
||||
->required()
|
||||
->type(Type::STRING)
|
||||
->min(1)
|
||||
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
|
||||
]);
|
||||
|
||||
$this->ruleset->POST([
|
||||
(new Rules(WorkTagsModel::REF_WORK_ID->value))
|
||||
->required()
|
||||
->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);
|
||||
private static function get_entity(): Response {
|
||||
return (new Call(Endpoints::WORK->value))->params([
|
||||
WorkModel::ID->value => $_POST[WorkTagsModel::REF_WORK_ID->value]
|
||||
])->get();
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
// Bail out if request validation failed
|
||||
if (!$this->ruleset->is_valid()) {
|
||||
return $this->resp_rules_invalid();
|
||||
// Bail out if work entity could not be fetched
|
||||
$entity = self::get_entity();
|
||||
if (!$entity->ok) {
|
||||
return $entity;
|
||||
}
|
||||
|
||||
// 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();
|
||||
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);
|
||||
}
|
||||
}
|
2
api/install.sh
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Install dependencies
|
||||
composer install --optimize-autoloader
|
27
api/src/Endpoints.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?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 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";
|
||||
}
|
|
@ -2,24 +2,38 @@
|
|||
|
||||
namespace VLW\API\Databases\VLWdb;
|
||||
|
||||
use libmysqldriver\MySQL;
|
||||
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";
|
||||
}
|
||||
|
||||
class VLWdb {
|
||||
const UUID_LENGTH = 36;
|
||||
|
||||
const MYSQL_INT_MAX_LENGTH = 2147483647;
|
||||
const MYSQL_TEXT_MAX_LENGTH = 65538;
|
||||
const MYSQL_VARCHAR_MAX_LENGTH = 255;
|
||||
const MYSQL_INT_MAX_LENGHT = 2147483647;
|
||||
const MYSQL_TINYINT_MAX_LENGTH = 255;
|
||||
|
||||
protected MySQL $db;
|
||||
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);
|
||||
|
||||
public function __construct() {
|
||||
// Create new MariaDB connection
|
||||
$this->db = new MySQL(
|
||||
$_ENV["vlwdb"]["mariadb_host"],
|
||||
$_ENV["vlwdb"]["mariadb_user"],
|
||||
$_ENV["vlwdb"]["mariadb_pass"],
|
||||
$_ENV["vlwdb"]["mariadb_db"],
|
||||
$_ENV["connect"]["host"],
|
||||
$_ENV["connect"]["user"],
|
||||
$_ENV["connect"]["pass"],
|
||||
$_ENV["databases"][$database->value],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -46,7 +60,24 @@
|
|||
);
|
||||
}
|
||||
|
||||
public static function is_mysqli_result(\mysqli_result|bool $resp): bool {
|
||||
return $resp instanceof \mysqli_result;
|
||||
// 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;
|
||||
}
|
||||
}
|
19
api/src/databases/models/Battlestation/Chassis.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?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";
|
||||
}
|
14
api/src/databases/models/Battlestation/Config/ChassisMb.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?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";
|
||||
}
|
15
api/src/databases/models/Battlestation/Config/Config.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?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";
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?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";
|
||||
}
|
23
api/src/databases/models/Battlestation/Config/MbDram.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?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";
|
||||
}
|
14
api/src/databases/models/Battlestation/Config/MbGpu.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?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";
|
||||
}
|
14
api/src/databases/models/Battlestation/Config/MbPsu.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?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";
|
||||
}
|
25
api/src/databases/models/Battlestation/Config/MbStorage.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?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";
|
||||
}
|
16
api/src/databases/models/Battlestation/Coolers.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?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";
|
||||
}
|
31
api/src/databases/models/Battlestation/Cpu.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?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";
|
||||
}
|
37
api/src/databases/models/Battlestation/Dram.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?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";
|
||||
}
|
20
api/src/databases/models/Battlestation/Gpu.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?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";
|
||||
}
|
30
api/src/databases/models/Battlestation/Mb.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?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";
|
||||
}
|
31
api/src/databases/models/Battlestation/Psu.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?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";
|
||||
}
|
45
api/src/databases/models/Battlestation/Storage.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?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";
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace VLW\API\Databases\VLWdb\Models\Coffee;
|
||||
|
||||
enum CoffeeModel: string {
|
||||
const TABLE = "coffee";
|
||||
|
||||
case ID = "id";
|
||||
case DATE_TIMESTAMP_CREATED = "date_timestamp_created";
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<?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";
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace VLW\API\Databases\VLWdb\Models\MediaSrcset;
|
||||
|
||||
enum MediaSrcsetModel: string {
|
||||
const TABLE = "media_srcset";
|
||||
|
||||
case ID = "id";
|
||||
case ANCHOR_DEFAULT = "anchor_default";
|
||||
}
|
|
@ -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_TIMESTAMP_CREATED = "date_timestamp_created";
|
||||
}
|
15
api/src/databases/models/Messages/Messages.php
Normal 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_CREATED = "date_created";
|
||||
}
|
|
@ -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_TIMESTAMP_MODIFIED = "date_timestamp_modified";
|
||||
case DATE_TIMESTAMP_CREATED = "date_timestamp_created";
|
||||
}
|
19
api/src/databases/models/Work/Work.php
Normal 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_MODIFIED = "date_modified";
|
||||
case DATE_CREATED = "date_created";
|
||||
}
|
3
api/src/databases/models/WorkActions.php → api/src/databases/models/Work/WorkActions.php
Executable file → Normal file
|
@ -5,8 +5,7 @@
|
|||
enum WorkActionsModel: string {
|
||||
const TABLE = "work_actions";
|
||||
|
||||
case ID = "id";
|
||||
case ANCHOR = "anchor";
|
||||
case REF_WORK_ID = "ref_work_id";
|
||||
case DISPLAY_TEXT = "display_text";
|
||||
case HREF = "href";
|
||||
case CLASS_LIST = "class_list";
|
0
api/src/databases/models/WorkMedia.php → api/src/databases/models/Work/WorkMedia.php
Executable file → Normal file
2
api/src/databases/models/WorkPermalinks.php → api/src/databases/models/Work/WorkPermalinks.php
Executable file → Normal file
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace VLW\API\Databases\VLWdb\Models\WorkPermalinks;
|
||||
namespace VLW\API\Databases\VLWdb\Models\Work;
|
||||
|
||||
enum WorkPermalinksModel: string {
|
||||
const TABLE = "work_permalinks";
|
20
api/src/databases/models/Work/WorkTags.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?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";
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
<?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";
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"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/"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
<?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";
|
||||
}
|
|
@ -1,325 +0,0 @@
|
|||
: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);
|
||||
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 {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
border-left: var(--border-style);
|
||||
grid-template-columns: 25px 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;
|
||||
}
|
||||
|
||||
/* ### 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;
|
||||
}
|
||||
|
||||
menu searchbox {
|
||||
display: none;
|
||||
}
|
||||
/* /> */
|
||||
}
|
|
@ -1,16 +1,8 @@
|
|||
@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");
|
||||
src:
|
||||
url("/assets/fonts/roboto-mono.woff2") format("woff2 supports variations"),
|
||||
url("/assets/fonts/roboto-mono.woff2") format("woff2-variations")
|
||||
;
|
||||
font-weight: 100 900;
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
--color-accent: rgb(var(--primer-color-accent));
|
||||
}
|
||||
|
||||
main {
|
||||
vv-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--padding);
|
||||
|
@ -15,7 +15,7 @@ main {
|
|||
|
||||
/* ## Divider */
|
||||
|
||||
main > hr {
|
||||
vv-shell > hr {
|
||||
border-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
|
@ -41,15 +41,15 @@ section.about span.interests {
|
|||
animation: interests-hue 5s infinite linear;
|
||||
}
|
||||
|
||||
/* ## Version */
|
||||
|
||||
section.version {
|
||||
color: rgba(255, 255, 255, .2);
|
||||
section.about p i:not(:hover) {
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
/* # 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: 50px;
|
||||
font-size: clamp(16px, 15vw, 50px);
|
||||
color: var(--color-accent);
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
|
@ -70,9 +70,7 @@ div.interests.active {
|
|||
}
|
||||
|
||||
div.interests p {
|
||||
--text-shadow-blur: 30px;
|
||||
|
||||
transition: 300ms transform;
|
||||
transition: 500ms transform cubic-bezier(.34,0,0,.99);
|
||||
position: absolute;
|
||||
text-shadow:
|
||||
0 0 var(--text-shadow-blur) black,
|
||||
|
|
45
assets/css/pages/about/battlestation-retired.css
Normal file
|
@ -0,0 +1,45 @@
|
|||
/* # Overrides */
|
||||
|
||||
:root {
|
||||
--primer-color-accent: 148, 255, 21;
|
||||
--color-accent: rgb(var(--primer-color-accent));
|
||||
}
|
||||
|
||||
vv-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--padding);
|
||||
}
|
||||
|
||||
/* # Content */
|
||||
|
||||
/* ## Title */
|
||||
|
||||
section.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* ## Actions */
|
||||
|
||||
section.actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--padding);
|
||||
}
|
||||
|
||||
/* # Size quries */
|
||||
|
||||
@media (max-width: 800px) {
|
||||
section.actions {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
section.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
290
assets/css/pages/about/battlestation.css
Normal file
|
@ -0,0 +1,290 @@
|
|||
/* # Overrides */
|
||||
|
||||
:root {
|
||||
--primer-color-accent: 148, 255, 21;
|
||||
--color-accent: rgb(var(--primer-color-accent));
|
||||
}
|
||||
|
||||
vv-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--padding);
|
||||
}
|
||||
|
||||
/* # Content */
|
||||
|
||||
/* ## Title */
|
||||
|
||||
section.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
padding: calc(var(--padding) * 1.5);
|
||||
background-color: rgba(var(--primer-color-accent), .1);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
section.title > div {
|
||||
margin-top: calc(var(--padding) / 2);
|
||||
display: flex;
|
||||
gap: var(--padding);
|
||||
}
|
||||
|
||||
/* ## Heading */
|
||||
|
||||
section.heading h1::before,
|
||||
section.heading h1::after {
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
section.heading h1::before {
|
||||
content: "“";
|
||||
}
|
||||
|
||||
section.heading h1::after {
|
||||
content: "”";
|
||||
}
|
||||
|
||||
/* ## Config */
|
||||
|
||||
section.config {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
gap: calc(var(--padding) * 2);
|
||||
}
|
||||
|
||||
section.config:nth-child(4n+2) {
|
||||
grid-template-columns: 1fr 300px;
|
||||
}
|
||||
|
||||
section.config:nth-child(4n+2) > svg {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
/* ### PC */
|
||||
|
||||
section.config > svg {
|
||||
position: sticky;
|
||||
top: calc(var(--running-size) + var(--padding));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
section.config > svg :is(rect, path) {
|
||||
transition: 300ms;
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
section.config > svg.active :is(rect, path),
|
||||
section.config > svg:hover :is(rect, path) {
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
section.config > svg g.active rect,
|
||||
section.config > svg g.active path,
|
||||
section.config > svg g:not(.group):hover rect,
|
||||
section.config > svg g:not(.group):hover path {
|
||||
opacity: 1;
|
||||
stroke: var(--color-accent);
|
||||
}
|
||||
|
||||
section.config > svg g.active rect,
|
||||
section.config > svg g:not(.group):hover rect {
|
||||
filter: drop-shadow(0 0 10px rgba(var(--primer-color-accent), .4));
|
||||
}
|
||||
|
||||
/* #### Case */
|
||||
|
||||
section.config g.case:not(:hover, .active) :is(rect, path) {
|
||||
opacity: .2;
|
||||
}
|
||||
|
||||
section.config > svg g.active path,
|
||||
section.config > svg g:not(.group):hover path {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
|
||||
/* #### Motherboard */
|
||||
|
||||
section.config > svg .mb .chips {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* #### Active states */
|
||||
|
||||
section.config > svg g:not(.group) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section.config[data-dram="1"] > svg g.drams g.dram:nth-child(1),
|
||||
section.config[data-dram="2"] > svg g.drams g.dram:nth-child(3n+1),
|
||||
section.config[data-dram="3"] > svg g.drams g.dram:nth-child(-n+3),
|
||||
section.config[data-dram="4"] > svg g.drams g.dram,
|
||||
|
||||
section.config[data-drives-mdottwo="1"] > svg g.mdottwo g.drive:nth-child(1),
|
||||
section.config[data-drives-mdottwo="2"] > svg g.mdottwo g.drive:nth-child(-n+2),
|
||||
section.config[data-drives-mdottwo="3"] > svg g.mdottwo g.drive:nth-child(-n+3),
|
||||
|
||||
section.config[data-drives-twodotfive="1"] > svg g.twodotfive g.drive:nth-child(1),
|
||||
section.config[data-drives-twodotfive="2"] > svg g.twodotfive g.drive:nth-child(-n+2),
|
||||
section.config[data-drives-twodotfive="3"] > svg g.twodotfive g.drive:nth-child(-n+3),
|
||||
|
||||
section.config[data-drives-threedotfive="1"] > svg g.threedotfive g.drive:nth-child(1),
|
||||
section.config[data-drives-threedotfive="2"] > svg g.threedotfive g.drive:nth-child(-n+2),
|
||||
section.config[data-drives-threedotfive="3"] > svg g.threedotfive g.drive:nth-child(-n+3),
|
||||
|
||||
section.config[data-mb="1"] > svg g.mb,
|
||||
section.config[data-psu="1"] > svg g.psu,
|
||||
section.config[data-gpu="1"] > svg g.gpu,
|
||||
section.config[data-cpu="1"] > svg g.cpu,
|
||||
section.config[data-case="1"] > svg g.case {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
/* ## Specs */
|
||||
|
||||
section.config .specs {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--padding) / 2);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
section.config .specs :is(.spec, .group) {
|
||||
--border-width: 4px;
|
||||
|
||||
transition: 300ms background-color, 300ms border-color, 500ms box-shadow;
|
||||
padding: calc(var(--padding) - var(--border-width));
|
||||
border: solid var(--border-width) transparent;
|
||||
background-color: rgba(255, 255, 255, .03);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
section.config .specs :is(.spec, .group) * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ### Active state */
|
||||
|
||||
section.config .specs.active {
|
||||
background-color: rgba(255, 255, 255, .03);
|
||||
}
|
||||
|
||||
section.config .specs.active :is(.group, .spec:not(.active)) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ### Spec */
|
||||
|
||||
section.config .specs .spec {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
section.config .specs .spec:hover {
|
||||
border-color: rgba(255, 255, 255, .05);
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
box-shadow: 0 0 30px 10px rgba(255, 255, 255, .05);
|
||||
}
|
||||
|
||||
section.config .specs .spec.active {
|
||||
border-color: var(--color-accent);
|
||||
background-color: rgba(var(--primer-color-accent), .1);
|
||||
box-shadow: 0 0 30px 10px rgba(var(--primer-color-accent), .05);
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
section.config .specs.active .spec.active {
|
||||
position: sticky;
|
||||
top: calc(var(--running-size) + var(--padding));
|
||||
}
|
||||
|
||||
section.config .specs .spec h3 {
|
||||
color: rgba(255, 255, 255, .3);
|
||||
}
|
||||
|
||||
section.config .specs .spec span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
section.config .specs .spec > div {
|
||||
display: none;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: calc(var(--padding) / 2);
|
||||
margin-top: var(--padding);
|
||||
}
|
||||
|
||||
section.config .specs .spec.active > div {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
section.config .specs .spec > div label {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
section.config .specs .spec > svg {
|
||||
display: none;
|
||||
height: calc(var(--padding) / 2);
|
||||
margin: 0 auto;
|
||||
margin-top: calc(var(--padding) / 2);
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
|
||||
/* ### Group */
|
||||
|
||||
section.config .specs .group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
section.config .specs .group.active {
|
||||
background-color: rgba(255, 255, 255, .2);
|
||||
}
|
||||
|
||||
section.config .specs .group:hover {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
section.config .specs .group.active:hover {
|
||||
background-color: rgba(255, 255, 255, .3);
|
||||
}
|
||||
|
||||
section.config .specs .group > svg {
|
||||
transition: 300ms transform;
|
||||
fill: var(--color-accent);
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
section.config .specs .group.active > svg {
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
|
||||
/* #### Collection */
|
||||
|
||||
section.config .specs .collection {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section.config .specs .group.active + .collection {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* # Size quries */
|
||||
|
||||
@media (max-width: 700px) {
|
||||
section.title > div {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
section.config,
|
||||
section.config:nth-child(4n+2) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
section.config > svg {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -5,19 +5,33 @@
|
|||
--color-accent: rgb(var(--primer-color-accent));
|
||||
}
|
||||
|
||||
main {
|
||||
vv-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--padding);
|
||||
}
|
||||
|
||||
.fingerprint {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* # Sections */
|
||||
|
||||
main > svg {
|
||||
vv-shell > svg {
|
||||
margin: var(--padding) 0;
|
||||
}
|
||||
|
||||
/* ## Modifiers */
|
||||
|
||||
section.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section.fade {
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
/* ## Social */
|
||||
|
||||
section.social {
|
||||
|
@ -61,11 +75,14 @@ section.social social.hovering p {
|
|||
display: initial;
|
||||
}
|
||||
|
||||
/* ## OpenPGP key */
|
||||
/* ## PGP 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);
|
||||
|
@ -81,13 +98,13 @@ section.pgp > svg {
|
|||
}
|
||||
|
||||
section.pgp > p {
|
||||
margin-bottom: var(--padding);
|
||||
padding: var(--padding);
|
||||
padding: 0 var(--padding);
|
||||
}
|
||||
|
||||
section.pgp .buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: var(--padding);
|
||||
gap: var(--padding);
|
||||
}
|
||||
|
||||
|
@ -158,6 +175,7 @@ section.form-message h3 {
|
|||
}
|
||||
|
||||
section.form-message pre {
|
||||
white-space: pre-wrap;
|
||||
padding: var(--padding);
|
||||
background-color: rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
@ -171,6 +189,10 @@ section.form-message.sent {
|
|||
background-color: var(--color-accent);
|
||||
}
|
||||
|
||||
section.form-message.sent + section.form {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* # Size queries */
|
||||
|
||||
@media (min-width: 460px) {
|
||||
|
|
|
@ -6,7 +6,7 @@ header {
|
|||
backdrop-filter: unset;
|
||||
}
|
||||
|
||||
main {
|
||||
vv-shell {
|
||||
max-width: unset;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
/* # Main styles */
|
||||
/* # Overrides */
|
||||
|
||||
body[vv-top-page="/"]::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* # vv-shell styles */
|
||||
|
||||
/* ## Picture */
|
||||
|
||||
main {
|
||||
vv-shell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
main img {
|
||||
vv-shell img {
|
||||
margin: auto;
|
||||
width: 25vh;
|
||||
pointer-events: none;
|
||||
|
@ -146,25 +152,33 @@ 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) {
|
||||
main {
|
||||
vv-shell {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
main img {
|
||||
vv-shell img {
|
||||
width: 35vh;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
section.search {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--padding);
|
||||
|
@ -21,6 +21,10 @@ section.search {
|
|||
margin-bottom: calc(var(--padding) * 2);
|
||||
}
|
||||
|
||||
vv-shell[vv-page="/search"] > section.search {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
section.search form {
|
||||
display: contents;
|
||||
}
|
||||
|
@ -31,6 +35,11 @@ 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"] {
|
||||
|
@ -42,10 +51,6 @@ section.search > svg {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
body:not([vv-page="/search"]) section.search {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* # Search results */
|
||||
|
||||
section.results .result {
|
||||
|
@ -54,19 +59,6 @@ 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 {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
--color-accent: rgb(var(--primer-color-accent));
|
||||
}
|
||||
|
||||
main {
|
||||
vv-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--padding);
|
||||
|
@ -28,6 +28,7 @@ section.git {
|
|||
}
|
||||
|
||||
section.git svg {
|
||||
fill: white;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
|
|
434
assets/css/shells/document.css
Normal file
|
@ -0,0 +1,434 @@
|
|||
:root {
|
||||
--primer-color-accent: 255, 255, 0;
|
||||
--color-accent: yellow;
|
||||
--hue-accent: 0deg;
|
||||
|
||||
--padding: 20px;
|
||||
--running-size: 80px;
|
||||
--header-search-size: var(--running-size);
|
||||
}
|
||||
|
||||
/* # 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::before {
|
||||
transition: 1s opacity;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -5%;
|
||||
right: 0;
|
||||
width: 20%;
|
||||
height: 5%;
|
||||
border-radius: 100%;
|
||||
z-index: 1000;
|
||||
box-shadow:
|
||||
0 0 30svh 10svh rgba(var(--primer-color-accent), .2),
|
||||
0 0 30svh 60svh rgba(var(--primer-color-accent), .1),
|
||||
0 0 30svh 150svh rgba(var(--primer-color-accent), .02)
|
||||
;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* "enable" the corner glow effect on initial load when a page has been fully loaded */
|
||||
body[vv-top-page]::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
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 {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
fill: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ### Inline */
|
||||
|
||||
button.inline {
|
||||
padding: calc(var(--padding) / 2) var(--padding);
|
||||
color: white;
|
||||
border: solid 2px white;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
button.inline.solid {
|
||||
color: black;
|
||||
border-color: var(--color-accent);
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
|
||||
a > button::after {
|
||||
content: " ➜";
|
||||
}
|
||||
|
||||
/* ### Text links */
|
||||
|
||||
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;
|
||||
grid-template-columns: 1fr var(--header-search-size) var(--running-size);
|
||||
grid-template-rows: var(--running-size);
|
||||
background-color: rgba(0, 0, 0, .8);
|
||||
z-index: 100;
|
||||
perspective: 3000px;
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header > * {
|
||||
--anim-3d-depth: 5px;
|
||||
--anim-3d-peek: 25deg;
|
||||
|
||||
transition: 300ms background-color;
|
||||
transform: rotateX(0deg);
|
||||
backface-visibility: hidden;
|
||||
box-shadow: 0 var(--anim-3d-depth) 0 0 rgba(255, 255, 255, .2);
|
||||
}
|
||||
|
||||
/* enable 3d flip animation */
|
||||
@media not (prefers-reduced-motion: reduce) {
|
||||
header > * {
|
||||
--transform-duration: 600ms;
|
||||
|
||||
transition: var(--transform-duration) transform, 300ms background-color;
|
||||
}
|
||||
}
|
||||
|
||||
header nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--padding);
|
||||
}
|
||||
|
||||
header .logo {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
header .logo path.stroke {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
|
||||
header header .search {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ### Buttons */
|
||||
|
||||
header button {
|
||||
--icon-size: 25px;
|
||||
|
||||
display: grid;
|
||||
width: 100%;
|
||||
border-left: var(--border-style);
|
||||
grid-template-columns: 1fr;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
padding: var(--padding);
|
||||
gap: var(--padding);
|
||||
fill: var(--color-accent);
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, .5);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
header button:not(.logo) svg {
|
||||
width: var(--icon-size);
|
||||
}
|
||||
|
||||
header button.search p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ### Searchbox */
|
||||
|
||||
header searchbox {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: var(--running-size);
|
||||
background-color: var(--color-accent);
|
||||
display: grid;
|
||||
align-items: stretch;
|
||||
grid-template-columns: 1fr var(--running-size);
|
||||
grid-template-rows: var(--running-size);
|
||||
box-shadow: none;
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
|
||||
header searchbox > * {
|
||||
box-shadow: 0 calc(var(--anim-3d-depth) * -1) 0 0 rgba(var(--primer-color-accent), .8);
|
||||
}
|
||||
|
||||
header searchbox button {
|
||||
transition: 300ms background-color, 300ms border-color;
|
||||
border-color: rgba(0, 0, 0, .1);
|
||||
fill: black;
|
||||
}
|
||||
|
||||
header searchbox input {
|
||||
padding: 0 var(--padding);
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
color: black;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* #### Active */
|
||||
|
||||
header.searchboxActive > * {
|
||||
transform: rotateX(-180deg);
|
||||
}
|
||||
|
||||
header.searchboxActive searchbox {
|
||||
transform: rotateX(0);
|
||||
}
|
||||
|
||||
/* ## vv-shell */
|
||||
|
||||
vv-shell {
|
||||
position: relative;
|
||||
padding: calc(var(--padding) * 1.5);
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
/* ## Search results */
|
||||
|
||||
search-results {
|
||||
transition: 500ms opacity, 300ms transform;
|
||||
position: fixed;
|
||||
top: var(--running-size);
|
||||
right: 0;
|
||||
width: 100%;
|
||||
padding: var(--padding);
|
||||
height: calc(100svh - var(--running-size));
|
||||
background-color: black;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transform: scale(.99);
|
||||
transform-origin: 100% 0;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
search-results:not([vv-page]) {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
header.searchboxActive ~ search-results {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* ### "Start typing" prompt */
|
||||
|
||||
search-results .info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
gap: 3svh;
|
||||
}
|
||||
|
||||
search-results .info :is(svg, img) {
|
||||
width: 128px;
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
|
||||
/* # 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.inline {
|
||||
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);
|
||||
}
|
||||
|
||||
header searchbox button:hover {
|
||||
background-color: rgba(0, 0, 0, .08);
|
||||
}
|
||||
|
||||
/* ### Search */
|
||||
|
||||
@media not (prefers-reduced-motion: reduce) {
|
||||
header:not(.searchboxActive) button.search:hover,
|
||||
header:not(.searchboxActive) button.search:hover + button.logo {
|
||||
transform: rotateX(calc(var(--anim-3d-peek) * -1));
|
||||
}
|
||||
|
||||
header:not(.searchboxActive) button.search:hover ~ searchbox {
|
||||
transform: rotateX(calc(180deg - var(--anim-3d-peek)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* # Size queries */
|
||||
|
||||
@media (min-width: 700px) {
|
||||
:root {
|
||||
--header-search-size: 250px;
|
||||
}
|
||||
|
||||
/* # Cornerstones */
|
||||
|
||||
body::before {
|
||||
right: unset;
|
||||
left: 0;
|
||||
box-shadow:
|
||||
0 0 30svh 10svh rgba(var(--primer-color-accent), .1),
|
||||
0 0 30svh 60svh rgba(var(--primer-color-accent), .05),
|
||||
0 0 30svh 150svh rgba(var(--primer-color-accent), .02)
|
||||
;
|
||||
}
|
||||
|
||||
/* ## Header */
|
||||
|
||||
header nav {
|
||||
margin: 0 calc(var(--padding) / 2);
|
||||
}
|
||||
|
||||
header > button.search {
|
||||
grid-template-columns: var(--icon-size) 1fr;
|
||||
}
|
||||
|
||||
header > button.search p {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
header.searchboxActive > nav {
|
||||
transform: rotateX(0deg);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* ### Searchbox */
|
||||
|
||||
header searchbox {
|
||||
width: calc(var(--header-search-size) + var(--running-size));
|
||||
}
|
||||
|
||||
/* ### Menu */
|
||||
|
||||
/* Move the search box to the header */
|
||||
header > button.search {
|
||||
display: grid;
|
||||
justify-items: baseline;
|
||||
}
|
||||
|
||||
@media (min-height: 600px) {
|
||||
search-results {
|
||||
top: calc(var(--running-size) + var(--padding));
|
||||
width: 50%;
|
||||
height: calc(100svh - 100px);
|
||||
background-color: rgba(0, 0, 0, .8);
|
||||
box-shadow:
|
||||
inset 0 0 100px 200px rgba(0, 0, 0, 1),
|
||||
0 0 100px 200px rgba(0, 0, 0, 1)
|
||||
;
|
||||
--webkit-backdrop-filter: blur(15px);
|
||||
backdrop-filter: blur(15px);
|
||||
}
|
||||
}
|
||||
}
|
BIN
assets/fonts/roboto-mono.woff2
Normal file
|
@ -1,72 +0,0 @@
|
|||
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 });
|
||||
}
|
|
@ -17,12 +17,8 @@ class Generator {
|
|||
get dir () { return this._dir; },
|
||||
set dir (newPath) {
|
||||
const url = new URL(newPath);
|
||||
url.pathname = this._dir_rel;
|
||||
|
||||
// Replace pathname of this file with relative path to assets
|
||||
const path = url.pathname.split("/");
|
||||
path[path.length - 1] = this._dir_rel;
|
||||
|
||||
url.pathname = path.join("/");
|
||||
this._dir = url.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,38 @@
|
|||
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 => {
|
||||
/*
|
||||
Generate random visuals for current element
|
||||
*/
|
||||
const size = element.getBoundingClientRect();
|
||||
|
||||
// Generate random HUE wheel rotation degrees
|
||||
const hue = randomIntFromInterval(0, 360);
|
||||
// Generate random element transform rotation
|
||||
const rotate = randomIntFromInterval(-5, 5);
|
||||
const transX = randomIntFromInterval(TRANS_LIMIT * -1, TRANS_LIMIT);
|
||||
const transY = randomIntFromInterval(TRANS_LIMIT * -1, TRANS_LIMIT);
|
||||
|
||||
// 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));
|
||||
|
||||
// Set initial position
|
||||
element.style.setProperty("top", `${originY}px`);
|
||||
element.style.setProperty("left", `${originX}px`);
|
||||
|
||||
// Set random HUE rotation
|
||||
// Set HUE rotation
|
||||
element.style.setProperty("-webkit-filter", `hue-rotate(${hue}deg)`);
|
||||
element.style.setProperty("filter", `hue-rotate(${hue}deg)`);
|
||||
|
||||
|
@ -39,10 +46,8 @@ const implodeInterests = () => {
|
|||
const wrapper = document.querySelector("div.interests");
|
||||
wrapper.classList.remove("active");
|
||||
|
||||
[...wrapper.querySelectorAll("p")].forEach(element => {
|
||||
// Reset to initial position
|
||||
element.style.setProperty("transform", "translate(0, 0)");
|
||||
});
|
||||
[...wrapper.querySelectorAll("p")].forEach(element => element.style.setProperty("transform", "translate(0, 0)"));
|
||||
};
|
||||
|
||||
// Bind triggers for interests explosion and implotion
|
||||
|
@ -55,10 +60,7 @@ const implodeInterests = () => {
|
|||
// Get absolute position of the trigger element
|
||||
const size = interestsElement.getBoundingClientRect();
|
||||
|
||||
const x = size.x - 80;
|
||||
const y = size.y - 10;
|
||||
|
||||
explodeInterests(x, y);
|
||||
explodeInterests(size.x, size.y);
|
||||
});
|
||||
|
||||
interestsElement.addEventListener(canHover ? "mouseleave" : "touchend", () => implodeInterests());
|
||||
|
|
0
assets/js/pages/about/battlestation-retired.js
Normal file
72
assets/js/pages/about/battlestation.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
|
||||
|
||||
new Elevent("click", document.querySelectorAll(".group"), (event) => {
|
||||
// Collapse self if already active and current target
|
||||
if (event.target.classList.contains("active")) {
|
||||
return event.target.classList.remove("active");
|
||||
}
|
||||
|
||||
// Collapse all and open current target
|
||||
[...event.target.closest(".specs").querySelectorAll(".group")].forEach(element => element.classList.remove("active"));
|
||||
event.target.classList.add("active");
|
||||
});
|
||||
|
||||
new Elevent("click", document.querySelectorAll(".spec"), (event) => {
|
||||
event.target.classList.add("active");
|
||||
event.target.addEventListener("mouseleave", () => event.target.classList.remove("active"));
|
||||
});
|
||||
|
||||
// Bind hover listeners for components in the SVGs
|
||||
[...document.querySelectorAll("section.config g:not(.group)")].forEach(element => {
|
||||
element.addEventListener("mouseenter", () => {
|
||||
// Find an element in the most adjacent speclist and highlighit it
|
||||
const target = element.closest("section.config").querySelector(`.spec[data-target="${element.dataset.target}"]`);
|
||||
// Get closest specs wrapper element
|
||||
const specsElement = target.closest(".specs");
|
||||
// Spec item is part of a collection, we need to expand the group if that is the case
|
||||
const collectionElement = target.closest(".collection") ?? null;
|
||||
// Don't close the group after hove ends
|
||||
let closeGroupOnLeave = false;
|
||||
|
||||
// Set fixed height on .specs wrapper to prevent glitchy page jumping when scrolled
|
||||
specsElement.style.setProperty("height", `${specsElement.offsetHeight}px`);
|
||||
target.classList.add("active");
|
||||
specsElement.classList.add("active");
|
||||
|
||||
if (collectionElement) {
|
||||
// Close the group on leave if the group wasn't active before hovering
|
||||
closeGroupOnLeave = !collectionElement.previousElementSibling.classList.contains("active");
|
||||
|
||||
collectionElement.previousElementSibling.classList.add("active");
|
||||
}
|
||||
|
||||
// Bind hover leave listener
|
||||
element.addEventListener("mouseleave", () => {
|
||||
// Reset to initial states
|
||||
target.classList.remove("active");
|
||||
specsElement.classList.remove("active");
|
||||
specsElement.style.removeProperty("height");
|
||||
|
||||
// Group was closed prior to hover, let's close it on hover leave
|
||||
if (closeGroupOnLeave) {
|
||||
collectionElement.previousElementSibling.classList.remove("active");
|
||||
}
|
||||
}, { once: true });
|
||||
});
|
||||
});
|
||||
|
||||
// Bind event listeners for components in the spec lists
|
||||
[...document.querySelectorAll("section.config .spec:not(.group)")].forEach(element => {
|
||||
element.addEventListener("mouseenter", () => {
|
||||
const svgTarget = element.closest("section.config").querySelector(`svg`);
|
||||
const target = svgTarget.querySelector(`svg g[data-target="${element.dataset.target}"]`);
|
||||
|
||||
svgTarget.classList.add("active");
|
||||
target.classList.add("active");
|
||||
|
||||
element.addEventListener("mouseleave", () => {
|
||||
svgTarget.classList.remove("active");
|
||||
target.classList.remove("active");
|
||||
}, { once: true });
|
||||
});
|
||||
});
|
|
@ -10,8 +10,6 @@ class ContactForm {
|
|||
[...document.querySelectorAll("form :is(input, textarea)")].forEach(element => {
|
||||
element.addEventListener("keyup", () => this.saveMessage());
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Get saved message as JSON from SessionStorage
|
||||
|
@ -36,6 +34,7 @@ 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;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
|
||||
|
||||
// Click to copy email button
|
||||
{
|
||||
const EMAIL_CPY_ANIM_DUR_MSECONDS = 1000;
|
||||
|
||||
// Run email copied splash animation
|
||||
|
@ -51,9 +55,7 @@ const emailCopiedAnimation = () => {
|
|||
}, EMAIL_CPY_ANIM_DUR_MSECONDS + 100);
|
||||
}
|
||||
|
||||
new vv.Interactions("index", {
|
||||
// Copy email address to clipboard
|
||||
copyEmail: async () => {
|
||||
new Elevent("click", document.querySelector(".email"), async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText("victor@vlw.se");
|
||||
|
||||
|
@ -70,14 +72,11 @@ new vv.Interactions("index", {
|
|||
} 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) => {
|
||||
|
@ -104,17 +103,6 @@ if (window.matchMedia("(hover: hover)")) {
|
|||
});
|
||||
|
||||
// Reset color on navigation
|
||||
document.querySelector(vv._env.MAIN).addEventListener(vv.Navigation.events.LOADED, () => updateColor(), { once: true });
|
||||
vv.Navigation.rootShellElement.addEventListener(vv.Navigation.EVENTS.STARTED, () => 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");
|
||||
});
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// 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 +0,0 @@
|
|||
new vv.Interactions("work");
|
50
assets/js/shells/document.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
|
||||
|
||||
const CLASSNAME_SEARCHBOX_ACTIVE = "searchboxActive";
|
||||
|
||||
// Handle search box open/close buttons
|
||||
{
|
||||
// Open search box
|
||||
new Elevent("click", document.querySelector(".searchbox-open"), () => {
|
||||
document.querySelector("header").classList.add(CLASSNAME_SEARCHBOX_ACTIVE);
|
||||
// Select searchbox inner input element
|
||||
document.querySelector("searchbox input").focus();
|
||||
});
|
||||
|
||||
// Close searchbox
|
||||
new Elevent("click", document.querySelector(".searchbox-close"), () => {
|
||||
// Disable search button interaction while animation is running
|
||||
// This is required to prevent conflicts with the :hover "peak" transformation
|
||||
const searchButtonElement = document.querySelector("header button.search");
|
||||
const transformDuration = parseInt(window.getComputedStyle(searchButtonElement).getPropertyValue("--transform-duration"));
|
||||
searchButtonElement.style.setProperty("pointer-events", "none");
|
||||
|
||||
document.querySelector("header").classList.remove(CLASSNAME_SEARCHBOX_ACTIVE);
|
||||
|
||||
// Wait for the transform animation to finish
|
||||
setTimeout(() => searchButtonElement.style.removeProperty("pointer-events"), transformDuration);
|
||||
});
|
||||
}
|
||||
|
||||
// Root shell navigation event handlers
|
||||
{
|
||||
// On all top shell navigations
|
||||
new Elevent(vv.Navigation.EVENTS.STARTED, vv.Navigation.rootShellElement, () => {
|
||||
// Close searchbox on top shell navigations
|
||||
document.querySelector("header").classList.remove(CLASSNAME_SEARCHBOX_ACTIVE);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle search logic
|
||||
{
|
||||
const searchResultsElement = document.querySelector("search-results");
|
||||
|
||||
document.querySelector("header input[type='search']").addEventListener("input", (event) => {
|
||||
// Debounce user input
|
||||
clearTimeout(event.target._throttle);
|
||||
event.target._throttle = setTimeout(() => {
|
||||
// Navigate search-results element on user input
|
||||
new vv.Navigation(`/search?q=${event.target.value}`).navigate(searchResultsElement);
|
||||
}, 100);
|
||||
});
|
||||
}
|
173
assets/media/battlestation.svg
Normal file
|
@ -0,0 +1,173 @@
|
|||
<svg viewBox="0 0 236 288" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask fill="#fff" id="a"><path d="M19 269a1 1 0 0 1-1-1v-11a1 1 0 0 1 1-1v13Z"></path></mask>
|
||||
<mask fill="#fff" id="b"><path d="M87 241a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1v-7Z"></path></mask>
|
||||
<mask fill="#fff" id="c"><path d="M87 251a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1v-7Z"></path></mask>
|
||||
<mask fill="#fff" id="d"><path d="M179 272.051c0-.58.471-1.051 1.051-1.051h8.898c.58 0 1.051.471 1.051 1.051h-11Z"></path></mask>
|
||||
<mask fill="#fff" id="e"><path d="M191 272.051c0-.58.471-1.051 1.051-1.051h2.898c.58 0 1.051.471 1.051 1.051h-5Z"></path></mask>
|
||||
<mask fill="#fff" id="f"><path d="M179 257.333c0-.58.471-1.051 1.051-1.051h8.898c.58 0 1.051.471 1.051 1.051h-11Z"></path></mask>
|
||||
<mask fill="#fff" id="g"><path d="M191 257.333c0-.58.471-1.051 1.051-1.051h2.898c.58 0 1.051.471 1.051 1.051h-5Z"></path></mask>
|
||||
<mask fill="#fff" id="h"><path d="M179 242.615c0-.58.471-1.051 1.051-1.051h8.898c.58 0 1.051.471 1.051 1.051h-11Z"></path></mask>
|
||||
<mask fill="#fff" id="i"><path d="M191 242.615c0-.58.471-1.051 1.051-1.051h2.898c.58 0 1.051.471 1.051 1.051h-5Z"></path></mask>
|
||||
<mask fill="#fff" id="j"><path d="M133 257.333c0-.58.471-1.051 1.051-1.051h8.898c.58 0 1.051.471 1.051 1.051h-11Z"></path></mask>
|
||||
<mask fill="#fff" id="k"><path d="M145 257.333c0-.58.471-1.051 1.051-1.051h2.898c.58 0 1.051.471 1.051 1.051h-5Z"></path></mask>
|
||||
<mask fill="#fff" id="l"><path d="M133 242.615c0-.58.471-1.051 1.051-1.051h8.898c.58 0 1.051.471 1.051 1.051h-11Z"></path></mask>
|
||||
<mask fill="#fff" id="m"><path d="M145 242.615c0-.58.471-1.051 1.051-1.051h2.898c.58 0 1.051.471 1.051 1.051h-5Z"></path></mask>
|
||||
<mask fill="#fff" id="n"><path d="M133 272.051c0-.58.471-1.051 1.051-1.051h8.898c.58 0 1.051.471 1.051 1.051h-11Z"></path></mask>
|
||||
<mask fill="#fff" id="o"><path d="M145 272.051c0-.58.471-1.051 1.051-1.051h2.898c.58 0 1.051.471 1.051 1.051h-5Z"></path></mask>
|
||||
<mask fill="#fff" id="p"><path d="M186 40a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1h-11Z"></path></mask>
|
||||
<mask fill="#fff" id="q"><path d="M198 40a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1h-5Z"></path></mask>
|
||||
<mask fill="#fff" id="r"><path d="M186 51a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1h-11Z"></path></mask>
|
||||
<mask fill="#fff" id="s"><path d="M198 51a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1h-5Z"></path></mask>
|
||||
<mask fill="#fff" id="t"><path d="M186 62a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1h-11Z"></path></mask>
|
||||
<mask fill="#fff" id="u"><path d="M198 62a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1h-5Z"></path></mask>
|
||||
<mask fill="#fff" id="v"><path d="M186 73a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1h-11Z"></path></mask>
|
||||
<mask fill="#fff" id="w"><path d="M198 73a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1h-5Z"></path></mask>
|
||||
<mask fill="#fff" id="x"><path d="M129.535 123.781h-1.082a.453.453 0 0 1 0-.906h1.082v.906Z"></path></mask>
|
||||
<mask fill="#fff" id="y"><path d="M129.535 115.625h-1.082a.453.453 0 0 1 0-.906h1.082v.906Z"></path></mask>
|
||||
<mask fill="#fff" id="z"><path d="M129.535 121.969H129a1 1 0 0 1-1-1v-3.438a1 1 0 0 1 1-1h.535v5.438Z"></path></mask>
|
||||
<mask fill="#fff" id="A"><path d="M92.535 123.781h-1.082a.453.453 0 0 1 0-.906h1.082v.906Z"></path></mask>
|
||||
<mask fill="#fff" id="B"><path d="M92.535 115.625h-1.082a.453.453 0 0 1 0-.906h1.082v.906Z"></path></mask>
|
||||
<mask fill="#fff" id="C"><path d="M92.535 121.969H92a1 1 0 0 1-1-1v-3.438a1 1 0 0 1 1-1h.535v5.438Z"></path></mask>
|
||||
<mask fill="#fff" id="D"><path d="M68 143v-2a1 1 0 0 1 1-1h29a1 1 0 0 1 1 1v2H68Z"></path></mask>
|
||||
<mask fill="#fff" id="E"><path d="M101 143v-2a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v2h-3Z"></path></mask>
|
||||
<mask fill="#fff" id="F"><path d="M64 143v-2a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v2h-3Z"></path></mask>
|
||||
<mask fill="#fff" id="G"><path d="M55.535 123.781h-1.082a.453.453 0 0 1 0-.906h1.082v.906Z"></path></mask>
|
||||
<mask fill="#fff" id="H"><path d="M55.535 115.625h-1.082a.453.453 0 0 1 0-.906h1.082v.906Z"></path></mask>
|
||||
<mask fill="#fff" id="I"><path d="M55.535 121.969H55a1 1 0 0 1-1-1v-3.438a1 1 0 0 1 1-1h.535v5.438Z"></path></mask>
|
||||
<mask fill="#fff" id="J"><path d="M76 38h1a1 1 0 0 1 1 1v33a1 1 0 0 1-1 1h-1V38Z"></path></mask>
|
||||
<mask fill="#fff" id="K"><path d="M76 76h1a1 1 0 0 1 1 1v22a1 1 0 0 1-1 1h-1V76Z"></path></mask>
|
||||
<mask fill="#fff" id="L"><path d="M60 38h1a1 1 0 0 1 1 1v33a1 1 0 0 1-1 1h-1V38Z"></path></mask>
|
||||
<mask fill="#fff" id="M"><path d="M60 76h1a1 1 0 0 1 1 1v22a1 1 0 0 1-1 1h-1V76Z"></path></mask>
|
||||
<mask fill="#fff" id="N"><path d="M137 100h-1a1 1 0 0 1-1-1V66a1 1 0 0 1 1-1h1v35Z"></path></mask>
|
||||
<mask fill="#fff" id="O"><path d="M137 62h-1a1 1 0 0 1-1-1V39a1 1 0 0 1 1-1h1v24Z"></path></mask>
|
||||
<mask fill="#fff" id="P"><path d="M155 100h-1a1 1 0 0 1-1-1V66a1 1 0 0 1 1-1h1v35Z"></path></mask>
|
||||
<mask fill="#fff" id="Q"><path d="M155 62h-1a1 1 0 0 1-1-1V39a1 1 0 0 1 1-1h1v24Z"></path></mask>
|
||||
<g data-target="case" data-index="1" class="case">
|
||||
<rect height="278" rx="3" stroke="#fff" stroke-width="6" width="230" x="3" y="7"></rect>
|
||||
<path d="M20.639 216.906a2 2 0 0 1 1.674-.906h192.14a2 2 0 0 1 1.756 1.043l2.18 4c.726 1.333-.239 2.957-1.756 2.957H19.697c-1.589 0-2.543-1.764-1.674-3.094l2.616-4ZM5.451.793A2 2 0 0 1 7.046 0h222.808a2 2 0 0 1 1.692.934l2.521 4c.84 1.331-.118 3.066-1.692 3.066H4.021C2.369 8 1.429 6.11 2.425 4.793l3.026-4Z" fill="#fff"></path>
|
||||
</g>
|
||||
<g data-target="psu" class="psu">
|
||||
<path d="M20 268v-11h-4v11h4Zm-3-12v13h4v-13h-4Zm3 1a1 1 0 0 1-1 1v-4a3 3 0 0 0-3 3h4Zm-1 10a1 1 0 0 1 1 1h-4a3 3 0 0 0 3 3v-4Z" fill="#fff" mask="url(#a)"></path>
|
||||
<path d="M86 242v5h4v-5h-4Zm3 6v-7h-4v7h4Zm-3-1a1 1 0 0 1 1-1v4a3 3 0 0 0 3-3h-4Zm1-4a1 1 0 0 1-1-1h4a3 3 0 0 0-3-3v4Z" fill="#fff" mask="url(#b)"></path>
|
||||
<path d="M86 252v5h4v-5h-4Zm3 6v-7h-4v7h4Zm-3-1a1 1 0 0 1 1-1v4a3 3 0 0 0 3-3h-4Zm1-4a1 1 0 0 1-1-1h4a3 3 0 0 0-3-3v4Z" fill="#fff" mask="url(#c)"></path>
|
||||
<rect height="38" rx="3.5" stroke="#fff" stroke-width="3" width="65" x="20.5" y="234.5"></rect>
|
||||
</g>
|
||||
<g class="group drives threedotfive">
|
||||
<g data-target="drive" class="drive">
|
||||
<rect height="9.564" rx="2" stroke="#fff" stroke-width="2" width="41" x="176" y="263.436"></rect>
|
||||
<path d="M180.051 273h8.898v-4h-8.898v4Zm9.949-2.949h-11v4h11v-4ZM188.949 273a.95.95 0 0 1-.949-.949h4a3.051 3.051 0 0 0-3.051-3.051v4Zm-8.898-4a3.051 3.051 0 0 0-3.051 3.051h4a.95.95 0 0 1-.949.949v-4Z" fill="#fff" mask="url(#d)"></path>
|
||||
<path d="M192.051 273h2.898v-4h-2.898v4Zm3.949-2.949h-5v4h5v-4ZM194.949 273a.95.95 0 0 1-.949-.949h4a3.051 3.051 0 0 0-3.051-3.051v4Zm-2.898-4a3.051 3.051 0 0 0-3.051 3.051h4a.95.95 0 0 1-.949.949v-4Z" fill="#fff" mask="url(#e)"></path>
|
||||
</g>
|
||||
<g data-target="drive" class="drive">
|
||||
<rect height="9.564" rx="2" stroke="#fff" stroke-width="2" width="41" x="176" y="248.718"></rect>
|
||||
<path d="M192.051 258.282h2.898v-4h-2.898v4Zm3.949-2.949h-5v4h5v-4Zm-1.051 2.949a.95.95 0 0 1-.949-.949h4a3.051 3.051 0 0 0-3.051-3.051v4Zm-2.898-4a3.051 3.051 0 0 0-3.051 3.051h4a.95.95 0 0 1-.949.949v-4Z" fill="#fff" mask="url(#g)"></path>
|
||||
<path d="M180.051 258.282h8.898v-4h-8.898v4Zm9.949-2.949h-11v4h11v-4Zm-1.051 2.949a.95.95 0 0 1-.949-.949h4a3.051 3.051 0 0 0-3.051-3.051v4Zm-8.898-4a3.051 3.051 0 0 0-3.051 3.051h4a.95.95 0 0 1-.949.949v-4Z" fill="#fff" mask="url(#f)"></path>
|
||||
</g>
|
||||
<g data-target="drive" class="drive">
|
||||
<rect height="9.564" rx="2" stroke="#fff" stroke-width="2" width="41" x="176" y="234"></rect>
|
||||
<path d="M192.051 243.564h2.898v-4h-2.898v4Zm3.949-2.949h-5v4h5v-4Zm-1.051 2.949a.95.95 0 0 1-.949-.949h4a3.051 3.051 0 0 0-3.051-3.051v4Zm-2.898-4a3.051 3.051 0 0 0-3.051 3.051h4a.95.95 0 0 1-.949.949v-4Z" fill="#fff" mask="url(#i)"></path>
|
||||
<path d="M180.051 243.564h8.898v-4h-8.898v4Zm9.949-2.949h-11v4h11v-4Zm-1.051 2.949a.95.95 0 0 1-.949-.949h4a3.051 3.051 0 0 0-3.051-3.051v4Zm-8.898-4a3.051 3.051 0 0 0-3.051 3.051h4a.95.95 0 0 1-.949.949v-4Z" fill="#fff" mask="url(#h)"></path>
|
||||
</g>
|
||||
<g data-target="drive" class="drive">
|
||||
<rect height="9.564" rx="2" stroke="#fff" stroke-width="2" width="41" x="130" y="248.718"></rect>
|
||||
<path d="M134.051 258.282h8.898v-4h-8.898v4Zm9.949-2.949h-11v4h11v-4Zm-1.051 2.949a.95.95 0 0 1-.949-.949h4a3.051 3.051 0 0 0-3.051-3.051v4Zm-8.898-4a3.051 3.051 0 0 0-3.051 3.051h4a.95.95 0 0 1-.949.949v-4Z" fill="#fff" mask="url(#j)"></path>
|
||||
<path d="M146.051 258.282h2.898v-4h-2.898v4Zm3.949-2.949h-5v4h5v-4Zm-1.051 2.949a.95.95 0 0 1-.949-.949h4a3.051 3.051 0 0 0-3.051-3.051v4Zm-2.898-4a3.051 3.051 0 0 0-3.051 3.051h4a.95.95 0 0 1-.949.949v-4Z" fill="#fff" mask="url(#k)"></path>
|
||||
</g>
|
||||
<g data-target="drive" class="drive">
|
||||
<rect height="9.564" rx="2" stroke="#fff" stroke-width="2" width="41" x="130" y="234"></rect>
|
||||
<path d="M134.051 243.564h8.898v-4h-8.898v4Zm9.949-2.949h-11v4h11v-4Zm-1.051 2.949a.95.95 0 0 1-.949-.949h4a3.051 3.051 0 0 0-3.051-3.051v4Zm-8.898-4a3.051 3.051 0 0 0-3.051 3.051h4a.95.95 0 0 1-.949.949v-4Z" fill="#fff" mask="url(#l)"></path>
|
||||
<path d="M146.051 243.564h2.898v-4h-2.898v4Zm3.949-2.949h-5v4h5v-4Zm-1.051 2.949a.95.95 0 0 1-.949-.949h4a3.051 3.051 0 0 0-3.051-3.051v4Zm-2.898-4a3.051 3.051 0 0 0-3.051 3.051h4a.95.95 0 0 1-.949.949v-4Z" fill="#fff" mask="url(#m)"></path>
|
||||
</g>
|
||||
<g data-target="drive" class="drive">
|
||||
<rect height="9.564" rx="2" stroke="#fff" stroke-width="2" width="41" x="130" y="263.436"></rect>
|
||||
<path d="M134.051 273h8.898v-4h-8.898v4Zm9.949-2.949h-11v4h11v-4ZM142.949 273a.95.95 0 0 1-.949-.949h4a3.051 3.051 0 0 0-3.051-3.051v4Zm-8.898-4a3.051 3.051 0 0 0-3.051 3.051h4a.95.95 0 0 1-.949.949v-4Z" fill="#fff" mask="url(#n)"></path>
|
||||
<path d="M146.051 273h2.898v-4h-2.898v4Zm3.949-2.949h-5v4h5v-4ZM148.949 273a.95.95 0 0 1-.949-.949h4a3.051 3.051 0 0 0-3.051-3.051v4Zm-2.898-4a3.051 3.051 0 0 0-3.051 3.051h4a.95.95 0 0 1-.949.949v-4Z" fill="#fff" mask="url(#o)"></path>
|
||||
</g>
|
||||
</g>
|
||||
<g class="group drives twodotfive">
|
||||
<g data-target="drive" class="drive">
|
||||
<rect height="5" rx="2" stroke="#fff" stroke-width="2" width="41" x="183" y="36"></rect>
|
||||
<path d="M187 41h9v-4h-9v4Zm10-3h-11v4h11v-4Zm-1 3a1 1 0 0 1-1-1h4a3 3 0 0 0-3-3v4Zm-9-4a3 3 0 0 0-3 3h4a1 1 0 0 1-1 1v-4Z" fill="#fff" mask="url(#p)"></path>
|
||||
<path d="M199 41h3v-4h-3v4Zm4-3h-5v4h5v-4Zm-1 3a1 1 0 0 1-1-1h4a3 3 0 0 0-3-3v4Zm-3-4a3 3 0 0 0-3 3h4a1 1 0 0 1-1 1v-4Z" fill="#fff" mask="url(#q)"></path>
|
||||
</g>
|
||||
<g data-target="drive" class="drive">
|
||||
<rect height="5" rx="2" stroke="#fff" stroke-width="2" width="41" x="183" y="47"></rect>
|
||||
<path d="M187 52h9v-4h-9v4Zm10-3h-11v4h11v-4Zm-1 3a1 1 0 0 1-1-1h4a3 3 0 0 0-3-3v4Zm-9-4a3 3 0 0 0-3 3h4a1 1 0 0 1-1 1v-4Z" fill="#fff" mask="url(#r)"></path>
|
||||
<path d="M199 52h3v-4h-3v4Zm4-3h-5v4h5v-4Zm-1 3a1 1 0 0 1-1-1h4a3 3 0 0 0-3-3v4Zm-3-4a3 3 0 0 0-3 3h4a1 1 0 0 1-1 1v-4Z" fill="#fff" mask="url(#s)"></path>
|
||||
</g>
|
||||
<g data-target="drive" class="drive">
|
||||
<rect height="5" rx="2" stroke="#fff" stroke-width="2" width="41" x="183" y="58"></rect>
|
||||
<path d="M187 63h9v-4h-9v4Zm10-3h-11v4h11v-4Zm-1 3a1 1 0 0 1-1-1h4a3 3 0 0 0-3-3v4Zm-9-4a3 3 0 0 0-3 3h4a1 1 0 0 1-1 1v-4Z" fill="#fff" mask="url(#t)"></path>
|
||||
<path d="M199 63h3v-4h-3v4Zm4-3h-5v4h5v-4Zm-1 3a1 1 0 0 1-1-1h4a3 3 0 0 0-3-3v4Zm-3-4a3 3 0 0 0-3 3h4a1 1 0 0 1-1 1v-4Z" fill="#fff" mask="url(#u)"></path>
|
||||
</g>
|
||||
<g data-target="drive" class="drive">
|
||||
<rect height="5" rx="2" stroke="#fff" stroke-width="2" width="41" x="183" y="69"></rect>
|
||||
<path d="M187 74h9v-4h-9v4Zm10-3h-11v4h11v-4Zm-1 3a1 1 0 0 1-1-1h4a3 3 0 0 0-3-3v4Zm-9-4a3 3 0 0 0-3 3h4a1 1 0 0 1-1 1v-4Z" fill="#fff" mask="url(#v)"></path>
|
||||
<path d="M199 74h3v-4h-3v4Zm4-3h-5v4h5v-4Zm-1 3a1 1 0 0 1-1-1h4a3 3 0 0 0-3-3v4Zm-3-4a3 3 0 0 0-3 3h4a1 1 0 0 1-1 1v-4Z" fill="#fff" mask="url(#w)"></path>
|
||||
</g>
|
||||
</g>
|
||||
<g data-target="mb" class="mb">
|
||||
<rect height="176" rx="3.5" stroke="#fff" stroke-width="3" width="152" x="22.5" y="25.5"></rect>
|
||||
<path d="m31.19 123.309-.01.007-7.32 5.782c-1.968 1.554-4.86.153-4.86-2.354V42a3 3 0 0 1 3-3h15a3 3 0 0 1 3 3v73.048c0 .931-.432 1.81-1.17 2.377l-7.64 5.884Z" fill="#fff"></path>
|
||||
<path d="m31.19 123.309-.01.007-7.32 5.782c-1.968 1.554-4.86.153-4.86-2.354V42a3 3 0 0 1 3-3h15a3 3 0 0 1 3 3v73.048c0 .931-.432 1.81-1.17 2.377l-7.64 5.884Z" stroke="#fff" stroke-width="2"></path>
|
||||
<g class="chips">
|
||||
<rect fill="#fff" height="13" rx="2" width="13" x="23" y="44"></rect>
|
||||
<rect fill="#fff" height="13" rx="2" width="13" x="23" y="61"></rect>
|
||||
<rect fill="#fff" height="13" rx="2" width="13" x="23" y="78"></rect>
|
||||
</g>
|
||||
</g>
|
||||
<g class="group drives mdottwo">
|
||||
<g data-target="drive" class="drive">
|
||||
<path d="M129.535 123.781v2h2v-2h-2Zm0-.906h2v-2h-2v2Zm0-1.094h-1.082v4h1.082v-4Zm-1.082 3.094h1.082v-4h-1.082v4Zm-.918-2v.906h4v-.906h-4Zm2.465.453c0 .854-.693 1.547-1.547 1.547v-4a2.453 2.453 0 0 0-2.453 2.453h4Zm-1.547-1.547c.854 0 1.547.693 1.547 1.547h-4a2.453 2.453 0 0 0 2.453 2.453v-4Z" fill="#fff" mask="url(#x)"></path>
|
||||
<path d="M129.535 115.625v2h2v-2h-2Zm0-.906h2v-2h-2v2Zm0-1.094h-1.082v4h1.082v-4Zm-1.082 3.094h1.082v-4h-1.082v4Zm-.918-2v.906h4v-.906h-4Zm2.465.453c0 .854-.693 1.547-1.547 1.547v-4a2.453 2.453 0 0 0-2.453 2.453h4Zm-1.547-1.547c.854 0 1.547.693 1.547 1.547h-4a2.453 2.453 0 0 0 2.453 2.453v-4Z" fill="#fff" mask="url(#y)"></path>
|
||||
<path d="M129.535 121.969v2h2v-2h-2Zm0-5.438h2v-2h-2v2Zm0 3.438H129v4h.535v-4Zm.465 1v-3.438h-4v3.438h4Zm-1-2.438h.535v-4H129v4Zm-1.465-2v5.438h4v-5.438h-4Zm2.465 1a1 1 0 0 1-1 1v-4a3 3 0 0 0-3 3h4Zm-1 2.438a1 1 0 0 1 1 1h-4a3 3 0 0 0 3 3v-4Z" fill="#fff" mask="url(#z)"></path>
|
||||
<rect height="12.5" rx="2" stroke="#fff" stroke-width="2" width="29.465" x="130.535" y="113"></rect>
|
||||
</g>
|
||||
<g data-target="drive" class="drive">
|
||||
<path d="M92.535 123.781v2h2v-2h-2Zm0-.906h2v-2h-2v2Zm0-1.094h-1.082v4h1.082v-4Zm-1.082 3.094h1.082v-4h-1.082v4Zm-.918-2v.906h4v-.906h-4Zm2.465.453c0 .854-.693 1.547-1.547 1.547v-4A2.453 2.453 0 0 0 89 123.328h4Zm-1.547-1.547c.854 0 1.547.693 1.547 1.547h-4a2.453 2.453 0 0 0 2.453 2.453v-4Z" fill="#fff" mask="url(#A)"></path>
|
||||
<path d="M92.535 115.625v2h2v-2h-2Zm0-.906h2v-2h-2v2Zm0-1.094h-1.082v4h1.082v-4Zm-1.082 3.094h1.082v-4h-1.082v4Zm-.918-2v.906h4v-.906h-4Zm2.465.453c0 .854-.693 1.547-1.547 1.547v-4A2.453 2.453 0 0 0 89 115.172h4Zm-1.547-1.547c.854 0 1.547.693 1.547 1.547h-4a2.453 2.453 0 0 0 2.453 2.453v-4Z" fill="#fff" mask="url(#B)"></path>
|
||||
<path d="M92.535 121.969v2h2v-2h-2Zm0-5.438h2v-2h-2v2Zm0 3.438H92v4h.535v-4Zm.465 1v-3.438h-4v3.438h4Zm-1-2.438h.535v-4H92v4Zm-1.465-2v5.438h4v-5.438h-4Zm2.465 1a1 1 0 0 1-1 1v-4a3 3 0 0 0-3 3h4Zm-1 2.438a1 1 0 0 1 1 1h-4a3 3 0 0 0 3 3v-4Z" fill="#fff" mask="url(#C)"></path>
|
||||
<rect height="12.5" rx="2" stroke="#fff" stroke-width="2" width="29.465" x="93.535" y="113"></rect>
|
||||
</g>
|
||||
<g data-target="drive" class="drive">
|
||||
<path d="M55.535 123.781v2h2v-2h-2Zm0-.906h2v-2h-2v2Zm0-1.094h-1.082v4h1.082v-4Zm-1.082 3.094h1.082v-4h-1.082v4Zm-.918-2v.906h4v-.906h-4Zm2.465.453c0 .854-.693 1.547-1.547 1.547v-4A2.453 2.453 0 0 0 52 123.328h4Zm-1.547-1.547c.854 0 1.547.693 1.547 1.547h-4a2.453 2.453 0 0 0 2.453 2.453v-4Z" fill="#fff" mask="url(#G)"></path>
|
||||
<path d="M55.535 115.625v2h2v-2h-2Zm0-.906h2v-2h-2v2Zm0-1.094h-1.082v4h1.082v-4Zm-1.082 3.094h1.082v-4h-1.082v4Zm-.918-2v.906h4v-.906h-4Zm2.465.453c0 .854-.693 1.547-1.547 1.547v-4A2.453 2.453 0 0 0 52 115.172h4Zm-1.547-1.547c.854 0 1.547.693 1.547 1.547h-4a2.453 2.453 0 0 0 2.453 2.453v-4Z" fill="#fff" mask="url(#H)"></path>
|
||||
<path d="M55.535 121.969v2h2v-2h-2Zm0-5.438h2v-2h-2v2Zm0 3.438H55v4h.535v-4Zm.465 1v-3.438h-4v3.438h4Zm-1-2.438h.535v-4H55v4Zm-1.465-2v5.438h4v-5.438h-4Zm2.465 1a1 1 0 0 1-1 1v-4a3 3 0 0 0-3 3h4Zm-1 2.438a1 1 0 0 1 1 1h-4a3 3 0 0 0 3 3v-4Z" fill="#fff" mask="url(#I)"></path>
|
||||
<rect height="12.5" rx="2" stroke="#fff" stroke-width="2" width="29.465" x="56.535" y="113"></rect>
|
||||
</g>
|
||||
</g>
|
||||
<g data-target="gpu" class="gpu">
|
||||
<path d="M59 148h86" stroke="#fff" stroke-linecap="round"></path>
|
||||
<path d="M59.5 136a1.5 1.5 0 0 0-3 0h3Zm0 8v-8h-3v8h3Z" fill="#fff"></path>
|
||||
<path d="M68 143v-2a1 1 0 0 1 1-1h29a1 1 0 0 1 1 1v2H68Z" mask="url(#D)" stroke="#fff" stroke-width="3"></path>
|
||||
<path d="M101 143v-2a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v2h-3Z" mask="url(#E)" stroke="#fff" stroke-width="3"></path>
|
||||
<path d="M64 143v-2a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v2h-3Z" mask="url(#F)" stroke="#fff" stroke-width="3"></path>
|
||||
<path d="M55 147a3 3 0 0 1 3-3h88a3 3 0 0 1 3 3v.525c0 .237-.028.473-.083.703l-2.764 11.474a3.001 3.001 0 0 1-2.917 2.298H58a3 3 0 0 1-3-3v-12Z" stroke="#fff" stroke-width="2"></path>
|
||||
</g>
|
||||
<g class="group drams">
|
||||
<g data-target="dram" class="dram">
|
||||
<path d="M60 38v-2h-2v2h2Zm0 35h-2v2h2v-2Zm0-33h1v-4h-1v4Zm0-1v33h4V39h-4Zm1 32h-1v4h1v-4Zm1 2V38h-4v35h4Zm-2-1a1 1 0 0 1 1-1v4a3 3 0 0 0 3-3h-4Zm1-32a1 1 0 0 1-1-1h4a3 3 0 0 0-3-3v4Z" fill="#fff" mask="url(#L)"></path>
|
||||
<path d="M60 76v-2h-2v2h2Zm0 24h-2v2h2v-2Zm0-22h1v-4h-1v4Zm0-1v22h4V77h-4Zm1 21h-1v4h1v-4Zm1 2V76h-4v24h4Zm-2-1a1 1 0 0 1 1-1v4a3 3 0 0 0 3-3h-4Zm1-21a1 1 0 0 1-1-1h4a3 3 0 0 0-3-3v4Z" fill="#fff" mask="url(#M)"></path>
|
||||
<rect height="66" rx="2" stroke="#fff" stroke-width="2" width="4" x="55" y="36"></rect>
|
||||
</g>
|
||||
<g data-target="dram" class="dram">
|
||||
<path d="M76 38v-2h-2v2h2Zm0 35h-2v2h2v-2Zm0-33h1v-4h-1v4Zm0-1v33h4V39h-4Zm1 32h-1v4h1v-4Zm1 2V38h-4v35h4Zm-2-1a1 1 0 0 1 1-1v4a3 3 0 0 0 3-3h-4Zm1-32a1 1 0 0 1-1-1h4a3 3 0 0 0-3-3v4Z" fill="#fff" mask="url(#J)"></path>
|
||||
<path d="M76 76v-2h-2v2h2Zm0 24h-2v2h2v-2Zm0-22h1v-4h-1v4Zm0-1v22h4V77h-4Zm1 21h-1v4h1v-4Zm1 2V76h-4v24h4Zm-2-1a1 1 0 0 1 1-1v4a3 3 0 0 0 3-3h-4Zm1-21a1 1 0 0 1-1-1h4a3 3 0 0 0-3-3v4Z" fill="#fff" mask="url(#K)"></path>
|
||||
<rect height="66" rx="2" stroke="#fff" stroke-width="2" width="4" x="71" y="36"></rect>
|
||||
</g>
|
||||
<g data-target="dram" class="dram">
|
||||
<path d="M137 100v2h2v-2h-2Zm0-35h2v-2h-2v2Zm0 33h-1v4h1v-4Zm0 1V66h-4v33h4Zm-1-32h1v-4h-1v4Zm-1-2v35h4V65h-4Zm2 1a1 1 0 0 1-1 1v-4a3 3 0 0 0-3 3h4Zm-1 32a1 1 0 0 1 1 1h-4a3 3 0 0 0 3 3v-4Z" fill="#fff" mask="url(#N)"></path>
|
||||
<path d="M137 62v2h2v-2h-2Zm0-24h2v-2h-2v2Zm0 22h-1v4h1v-4Zm0 1V39h-4v22h4Zm-1-21h1v-4h-1v4Zm-1-2v24h4V38h-4Zm2 1a1 1 0 0 1-1 1v-4a3 3 0 0 0-3 3h4Zm-1 21a1 1 0 0 1 1 1h-4a3 3 0 0 0 3 3v-4Z" fill="#fff" mask="url(#O)"></path>
|
||||
<rect height="66" rx="2" stroke="#fff" stroke-width="2" transform="rotate(180 142 102)" width="4" x="142" y="102"></rect>
|
||||
</g>
|
||||
<g data-target="dram" class="dram">
|
||||
<path d="M155 100v2h2v-2h-2Zm0-35h2v-2h-2v2Zm0 33h-1v4h1v-4Zm0 1V66h-4v33h4Zm-1-32h1v-4h-1v4Zm-1-2v35h4V65h-4Zm2 1a1 1 0 0 1-1 1v-4a3 3 0 0 0-3 3h4Zm-1 32a1 1 0 0 1 1 1h-4a3 3 0 0 0 3 3v-4Z" fill="#fff" mask="url(#P)"></path>
|
||||
<path d="M155 62v2h2v-2h-2Zm0-24h2v-2h-2v2Zm0 22h-1v4h1v-4Zm0 1V39h-4v22h4Zm-1-21h1v-4h-1v4Zm-1-2v24h4V38h-4Zm2 1a1 1 0 0 1-1 1v-4a3 3 0 0 0-3 3h4Zm-1 21a1 1 0 0 1 1 1h-4a3 3 0 0 0 3 3v-4Z" fill="#fff" mask="url(#Q)"></path>
|
||||
<rect height="66" rx="2" stroke="#fff" stroke-width="2" transform="rotate(180 160 102)" width="4" x="160" y="102"></rect>
|
||||
</g>
|
||||
</g>
|
||||
<g data-target="cpu" class="cpu">
|
||||
<path d="M98 56a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1h-3ZM113 56a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1h-3ZM103 56a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1h-3ZM108 56a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1h-3ZM116 82a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1h3ZM101 82a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1h3ZM111 82a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1h3ZM106 82a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1h3ZM120 60a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1v-3ZM120 75a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1v-3ZM120 65a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1v-3ZM120 70a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1v-3ZM94 78a1 1 0 0 1-1-1v-1a1 1 0 0 1 1-1v3ZM94 63a1 1 0 0 1-1-1v-1a1 1 0 0 1 1-1v3ZM94 73a1 1 0 0 1-1-1v-1a1 1 0 0 1 1-1v3ZM94 68a1 1 0 0 1-1-1v-1a1 1 0 0 1 1-1v3Z" fill="#fff"></path>
|
||||
<rect height="24" rx="3" stroke="#fff" stroke-width="2" width="24" x="95" y="57"></rect>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 19 KiB |
1
assets/media/icons/chevron.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 78.743 46.968" xmlns="http://www.w3.org/2000/svg"><path d="m530.436 290.342 6.658 8.935c4.438 5.956 8.512 11.037 12.222 15.242 3.71 4.205 7.691 8.797 11.945 13.776a590.023 590.023 0 0 1 11.95 14.425c3.713 4.638 9.56 11.441 17.54 20.41 7.98 8.97 15.892 17.872 23.736 26.707a2267.256 2267.256 0 0 0 21.633 24.05c6.578 7.197 11.225 8.056 13.942 2.576 2.717-5.48 8.45-12.8 17.199-21.96 8.748-9.162 15.904-16.447 21.468-21.856a2287.547 2287.547 0 0 1 17.272-16.611 674.569 674.569 0 0 0 16.506-16.229c5.054-5.154 9.753-9.968 14.098-14.443 4.344-4.475 9.494-10.016 15.448-16.624 5.954-6.607 10.747-11.748 14.38-15.422 3.631-3.675 6.18-6.009 7.645-7.003a16.094 16.094 0 0 1 4.757-2.202 16.095 16.095 0 0 1 5.212-.568c1.768.096 3.474.47 5.12 1.126a16.094 16.094 0 0 1 4.492 2.701 16.094 16.094 0 0 1 3.394 3.995 16.094 16.094 0 0 1 1.941 4.87c.381 1.729.476 3.473.285 5.234a16.094 16.094 0 0 1-1.402 5.051 16.095 16.095 0 0 1-2.94 4.34c-15.004 15.842-39.417-8.906-22.252-23.487a16.094 16.094 0 0 1 4.491-2.703 16.094 16.094 0 0 1 5.12-1.127 16.095 16.095 0 0 1 5.211.567 16.095 16.095 0 0 1 4.758 2.2 16.094 16.094 0 0 1 3.805 3.606 16.094 16.094 0 0 1 2.456 4.631c.565 1.678.848 3.403.848 5.173 0 1.771-.283 3.495-.849 5.173a16.095 16.095 0 0 1-2.455 4.632l-1.606 2.112-5.37 4.582c-3.583 3.055-8.51 7.767-14.786 14.135a11933.445 11933.445 0 0 0-15.839 16.1c-4.284 4.364-8.943 9.26-13.975 14.688a75638.076 75638.076 0 0 1-15.826 17.063 2642.057 2642.057 0 0 0-15.89 17.273c-5.073 5.567-11.507 12.866-19.3 21.897-7.794 9.03-13.63 15.948-17.51 20.753a496.057 496.057 0 0 1-12.49 14.836c-4.447 5.085-9.976 7.77-16.588 8.052-6.612.283-12.583-2.325-17.913-7.822-5.33-5.498-11.117-11.861-17.361-19.091-6.245-7.23-11.274-13.11-15.087-17.64a385.872 385.872 0 0 0-11.496-13.06 905.91 905.91 0 0 1-12.117-13.426c-4.228-4.774-10.087-11.613-17.577-20.518-7.49-8.905-13.37-15.797-17.635-20.676-4.266-4.88-8.437-9.663-12.511-14.35-4.075-4.687-8.453-9.915-13.135-15.684-4.681-5.77-7.355-9.208-8.02-10.317a15.914 15.914 0 0 1-1.567-3.517 15.937 15.937 0 0 1 .724-11.305 15.914 15.914 0 0 1 2.003-3.288 15.914 15.914 0 0 1 2.733-2.713 15.913 15.913 0 0 1 3.302-1.98 15.914 15.914 0 0 1 3.68-1.132 15.914 15.914 0 0 1 3.845-.219c1.29.083 2.552.319 3.785.708 1.233.39 2.401.92 3.505 1.593a15.914 15.914 0 0 1 3.023 2.386l1.366 1.375z" transform="matrix(.26458 0 0 .26458 -132.758 -75.015)"/></svg>
|
After Width: | Height: | Size: 2.3 KiB |
1
assets/media/icons/codeberg.svg
Normal file
After Width: | Height: | Size: 5.6 KiB |
|
@ -1 +0,0 @@
|
|||
<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>
|
Before Width: | Height: | Size: 957 B |
Before Width: | Height: | Size: 6.8 KiB |
1
assets/media/icons/matrix.svg
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
assets/media/ogp.jpg
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
assets/media/travolta.gif
Normal file
After Width: | Height: | Size: 238 KiB |