Compare commits

...

37 commits

Author SHA1 Message Date
e8a81b789b chore: bump Vegvisir to 3.2.8 (#52)
Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/52
2025-09-11 12:31:53 +02:00
5e9317def5 fix: enable strict parameter checks for /coffee POST endpoint (#51)
This will prevent typos from slipping through, which is nice.

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/51
2025-08-30 08:27:36 +02:00
8209ea5ecc fix: /coffee DELETE endpoint incorrectly configured and missing table reference (#50)
Fix class config and missing `from()` method for the `/coffee` DELETE endpoint

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/50
2025-08-30 08:27:12 +02:00
85e8e00091 feat(api): create DateTimeImmutable from timestamp for coffee POST endpoint (#49)
This PR allows creation of coffee entities at a specific date from a Unix timestamp by passing an integer to `date_created` when making a POST request to the `/coffee` endpoint.

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/49
2025-08-24 03:40:58 +02:00
vlw
d4d73e9278 feat: add install script (#48)
This PR adds an install script which will configure vlw.se along with Reflect and Vegvisir. The only step which is manual (maybe for now) is pointing the web and REST API server. This is mentioned at the end of the installation.

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/48
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2025-08-07 13:00:10 +02:00
fafa8c5852 fix: search minimum length offset and merge oversight (#47)
Fixed merge derp from #46 and minimum search query length offset

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/47
2025-08-02 14:58:36 +02:00
vlw
03492df615 content: replace ISO 8601 with RFC 3339 on the /about page interests (#45)
I'm afraid my relationship with ISO 8601 has come to a quiet end. RFC 3339 is my new best friend.

![Screenshot 2025-06-22 at 12-12-43 RFC 3339 vs ISO 8601.png](/attachments/7a914db9-12b5-4df6-955b-2ff5924f64fb)
https://ijmacd.github.io/rfc3339-iso8601/

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/45
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2025-08-02 13:33:19 +02:00
37f2ac00c3
refactor: database models and views inc. bump to PHP 8.4 (#46) 2025-08-02 13:30:23 +02:00
d5cc7fa82c feat: add animated dashoffset to SVG spacers on index page (#44)
This PR adds a simple slow moving `dashoffset` animation to the SVG spacers on the `/index` page.

<video src="https://blob.vlw.se/01974ec8-1da9-74b3-9c8b-0ef06ce6cd16.mp4" controls></video>

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/44
2025-06-12 10:33:50 +02:00
17bd93dca9 content: update about page texts and change forgejo link on work timeline (#42)
Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/42
2025-06-03 15:28:19 +02:00
6b5eee505a content: change the PGP text on the /contact page (#41)
Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/41
2025-06-03 15:27:45 +02:00
cd3139e778
chore: use Reflect built-in request validation (#37)
commit 15a1fb886a
Author: Victor Westerlund <victor@vlw.se>
Date:   Fri Apr 4 07:33:31 2025 +0200

    upstream(reflect): use Reflect built-in request validation
2025-04-19 13:13:28 +02:00
afe450a6d2 fix: WebKit variable font transition issue on index page (#38)
Closes #17

Also fixed this placeholder text contrast issue in Safari
![image](/attachments/e028f967-3d0f-41a8-b068-1bf067d61631)
to:
![image](/attachments/97b3e36f-277b-4646-b5fd-c45d53fd3ce7)

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/38
2025-04-19 11:06:46 +00:00
b398354594 chore(upstream): add support for Vegvisir 3.2 (#39)
[Add support for Vegvisir 3.2.0](https://codeberg.org/vegvisir/vegvisir/releases/tag/3.2.0)

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/39
2025-04-19 11:06:23 +00:00
62ddd25f38 fix: removed undeclared variable from search index endpoint (#36)
Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/36
2025-04-02 12:56:18 +00:00
fef10a8ea8 feat: add endpoint for updating runtime stats (#35)
Replacing all GET update params with a single update endpoint that calls POST on all endpoints that trigger runtime stats

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/35
2025-04-02 12:55:42 +00:00
a6c74f5c4f feat: add web highlights section to work page (#34)
This PR adds a "web highlights" section to the work page.

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/34
2025-03-29 07:34:54 +00:00
f60a27855d feat: add work references and remove default work timeline button (#33)
Added references to some new repos that I've created
- [vlw/big-black-coffee-button](https://codeberg.org/vlw/big-black-coffee-button)
- [vlw/href](https://codeberg.org/vlw/href)
- [vlw/curl](https://codeberg.org/vlw/curl)

I've also removed the 'read more' button that showed up in the work timeline for items that didn't have any actions defined. It's probably better to have no "read more" button at all than having it link to a "coming soon" page.

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/33
2025-03-29 07:34:39 +00:00
c5c7aaa919 fix(content): change about-me page texts and fix interest anim origin (#32)
More changes to the about page texts and also made the "interests explosion" effect center on pointer position instead of center of span, which caused some glitchy looking behavior when the text wraps. Also added "digital archiving" to the interests list!

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/32
2025-03-29 07:34:14 +00:00
f4279c0343 feat: add coffee stats endpoints and counter to about page (#28)
This PR refactors some texts on the about page (again), and also a adds two new endpoints for a database table that I have now made public that tracks the coffee cups I've had. The endpoint itself is not public now but I might make a page (something like `/about/coffee`) that presents it in a not-ugly way.

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/28
2025-03-13 15:16:53 +00:00
vlw
f76962e2ac feat: version control database seeds (#29)
This PR replaces the database files from Release with VCS in git

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/29
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2025-03-13 15:16:31 +00:00
5c7c9d2d3a chore: clean up MORE missed references to battlestation (#31)
Apparently I didn't get em' all in #27

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/31
2025-03-13 15:16:06 +00:00
041d175757 fix: rfc9116 compatible and signed security.txt (#26)
Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/26
2025-03-05 12:37:49 +00:00
vlw
0bb7a3a8be fix: allow index of everything in robots.txt (#25)
Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/25
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2025-03-05 12:37:21 +00:00
0baa6f8d85 chore: clean up some missed references to battlestation (#27)
It looks like I missed a few references to the "battlestation" pages and API that I removed in #24

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/27
2025-03-05 12:37:01 +00:00
4b71bbd10e fix(content): remove "battlestation" pages (#24)
Its time to retire the "battlestation" pages that showed some of the computers I've built using funny graphics. I don't feel like maintaining these pages, at least not on my main website. I might create a separate site for stuff like this and list it under the [playground pages](#19) when that gets merged.

I attached a video of what it looked like, and also its database for reference.

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/24
2025-02-05 04:58:13 +00:00
56cf142e0d refactor: major refactor, design overhaul and merge of Reflect API and Vegvisir sources into the same root (#23)
The PR is a huge refactor of all Reflect and Vegvisir code. I've merged the API and "Front-end" codebases together into the same root, this will allow for both Reflect and Vegvisir to use the same resources. Not only that, but I've also added proper database modeling with actual OOP inheritance for database tables.

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/23
2025-02-05 04:49:23 +00:00
2fa62991f9 feat: scroll page to top on Vegvisir navigations (#21)
Simple but effective

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/21
2025-01-28 15:29:48 +00:00
359342c7f7 chore(content): rewording of the encryption text on the contact page (#18)
Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/18
2025-01-28 15:25:22 +00:00
e420f33d4a feat: new design for button.inline elements (#22)
Redesign of the buttons I use on the site here and there. This changes all: `button.inline` and `button.inline.solid`

I think the new buttons look more "cutesy" or "cozy" with my hand drawn little icons, soft gradients and shadows, and larger size.

# Old
![image](/attachments/9c1082aa-551f-43f8-9428-89c190178335)
![image](/attachments/0cdb44fd-6378-4b22-8493-a2f52e1dfe65)
![image](/attachments/736c44d6-3bf4-4f83-a48c-da40008b7aed)

# New
![image](/attachments/9f4de1d9-8b60-4b54-ba2f-8166eb1126c1)
![image](/attachments/c42a20e6-a8ae-4b27-bcfa-b064e365a7c7)
![image](/attachments/8105c00d-caf3-4a9e-8e2d-0140cbc44caa)

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/22
2025-01-28 15:23:55 +00:00
e25b1b6689 feat: add language chart to about page (#14)
Replaces this section on the `/about` page:
![image](/attachments/67ac2f42-3784-4c69-9240-0a7961afb47d)
with:
![image](/attachments/fa073c9c-a016-4281-a3fb-30b7be95881f)

I will replace and fix the colors of the buttons after #15 is merged.

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/14
2025-01-28 14:45:52 +00:00
3b51458dd4 feat: featured works on the /work landingpage and moved timeline (#16)
Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/16
2025-01-28 14:45:00 +00:00
vlw
ff7d4f5397 chore: bump supported Vegvisir version (#13)
Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/13
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2024-11-18 21:26:46 +00:00
07e0046828 chore(content): fix pathname to Codeberg logo embed on /work (#12)
Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/12
Co-authored-by: Victor Westerlund <victor.vesterlund@gmail.com>
Co-committed-by: Victor Westerlund <victor.vesterlund@gmail.com>
2024-11-18 21:11:10 +00:00
18e1ae5088 fix: order work by descending date_created (#9)
Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/9
Co-authored-by: Victor Westerlund <victor.vesterlund@gmail.com>
Co-committed-by: Victor Westerlund <victor.vesterlund@gmail.com>
2024-11-18 21:00:08 +00:00
9e3c549e0a fix(content): rewording of the Codeberg banner text on the work page (#10)
Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/10
Co-authored-by: Victor Westerlund <victor.vesterlund@gmail.com>
Co-committed-by: Victor Westerlund <victor.vesterlund@gmail.com>
2024-11-18 20:59:17 +00:00
80c6579136 chore: add support for Vegvisir 3.1 (#11)
This PR adds basic support for the upcoming release Vegvisir 3.1

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/11
Co-authored-by: Victor Westerlund <victor.vesterlund@gmail.com>
Co-committed-by: Victor Westerlund <victor.vesterlund@gmail.com>
2024-11-18 20:53:29 +00:00
184 changed files with 3258 additions and 4652 deletions

View file

@ -1,7 +1,15 @@
[api]
base_url = "https://api.vlw.one/"
api_key = ""
verify_peer = 0
[mariadb]
host = ""
user = ""
pass = ""
db = ""
[time]
date_time_zone = "Europe/Stockholm"
[config_time_available]
time_zone = "Europe/Stockholm"
available_to_hour = 0;
reply_average_hours = 0;
available_from_hour = 0;
[service_forgejo]
url = ""
profiles = ""

24
.gitignore vendored
View file

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

6
.gitmodules vendored Normal file
View file

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

View file

@ -1,73 +1,27 @@
# vlw.se
This is the source code behind [vlw.se](https://vlw.se) which has been written from the ground up by me. This website is built on top of my [Vegvisir web framework](https://vegvisir.vlw.se) and my [Reflect API framework](https://reflect.vlw.se).
This is the source code behind [vlw.se](https://vlw.se) which is my personal website that I have written and designed from the ground up. The website is built on top of my own [Vegvisir web framework](https://vegvisir.vlw.se) and its optional REST API is built on top of my [Reflect API framework](https://reflect.vlw.se).
# Installation
If you for whatever reason want to get this website up and running for yourself this is how that is done.
Here's how you get my website up and running on your own machine. Note, I have only tested this on Linux and the install script we will run requires Bash with `coreutils` installed.
## 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/)
## Prerequisites
- A web server
- A MariaDB/MySQL server
- PHP 8.4 or newer with the following extensions enabled:
- - `php8.4-mysql`
- - `php8.4-mbstring`
- The composer package manager
- Bash with `coreutils` installed (for the install script)
**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)
## 1. Clone this repo
Clone this repository with its submodules. Preferably to a non-public directory - the frameworks will handle that.
```
git clone https://codeberg.org/vlw/vlw.se --recurse-submodules --depth 1
```
## Website (Vegvisir)
1. **Download this repo**
Git clone or download this repo to any local folder
```
git clone https://codeberg.org/vlw/vlw.se
```
2. **Download and install Vegvisir**
Follow the installation instructions for [Vegvisir](https://vegvisir.vlw.se/docs/installation) and point the `root_path` variable to your local vlw.se folder.
3. **Run the install script**
This bash script will install dependencies and make npm modules public.
```
./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.
## API (Reflect)
The API (and database) is where most content is stored and served from on this website.
1. **Download this repo**
**You can skip this if you've already downloaded the repo from step 1 in the website installation.**
Otherwise... Git clone or download this repo to any local folder
```
git clone https://codeberg.org/vlw/vlw.se
```
2. **Download and install Reflect**
Follow the installation instructions for [Reflect](https://reflect.vlw.se/docs/installation) and point the `endpoints` variable to the `/api` subdirectory in the local vlw.se folder.
3. **Install dependencies**
`cd` into the api folder and install dependencies with composer.
```
composer install --optimize-autoloader
```
4. **Create and import database**
[Create and] import the two databases associated with vlw.se data and the Reflect API configurations from `.sql` files on the Releases page.
5. **Set environment variables**
Make a copy of `/api/.env.example.ini` and change the `[vlwdb]` variables with your MariaDB credentials.
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.
## 2. Run the install script
Run the `install.sh` script from the root directory of this repository.
```
./install.sh
```
This script will install and configure Vegvisir, Reflect, and the website through a few propmpted steps.

View file

@ -1,8 +0,0 @@
[connect]
host = ""
user = ""
pass = ""
[databases]
vlw = ""
battlestation = ""

29
api/coffee/DELETE.php Normal file
View file

@ -0,0 +1,29 @@
<?php
use Reflect\{Response, Path};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\API;
use VLW\Helpers\UUID;
use VLW\Database\Models\Coffee\Coffee;
use VLW\Database\Tables\Coffee\Coffee as CoffeeTable;
require_once Path::root("src/API/API.php");
require_once Path::root("src/Database/Models/Coffee/Coffee.php");
require_once Path::root("src/Database/Tables/Coffee/Coffee.php");
final class DELETE_Coffee extends API {
public function __construct() {
parent::__construct(new Ruleset()->GET([
new Rules(CoffeeTable::ID->value)
->required()
->type(Type::STRING)
->min(UUID::LENGTH)
->max(UUID::LENGTH)
]));
}
public function main(): Response {
return new Response(new Coffee($_GET[CoffeeTable::ID->value])->delete());
}
}

19
api/coffee/GET.php Normal file
View file

@ -0,0 +1,19 @@
<?php
use Reflect\{Response, Path};
use VLW\API\API;
use VLW\Database\Models\Coffee\Coffee;
require_once Path::root("src/API/API.php");
require_once Path::root("src/Database/Models/Coffee/Coffee.php");
final class GET_Coffee extends API {
public function __construct() {
parent::__construct();
}
public function main(): Response {
return new Response(Coffee::all());
}
}

41
api/coffee/POST.php Normal file
View file

@ -0,0 +1,41 @@
<?php
use Reflect\{Response, Path};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\API;
use VLW\Database\Models\Coffee\Coffee;
use VLW\Database\Tables\Coffee\Coffee as CoffeeTable;
require_once Path::root("src/API/API.php");
require_once Path::root("src/Database/Models/Coffee/Coffee.php");
require_once Path::root("src/Database/Tables/Coffee/Coffee.php");
final class POST_Coffee extends API {
public function __construct() {
parent::__construct(new Ruleset(strict: true)->POST([
new Rules(CoffeeTable::DATE_CREATED->value)
->type(Type::STRING)
->type(Type::NUMBER)
->default(null)
]));
}
public function main(): Response {
$datetime = new DateTimeImmutable();
// Parse DateTime from POST string
if ($_POST[CoffeeTable::DATE_CREATED->value]) {
try {
// Create DateTimeImmutable from Unix timestamp or datetime string
$datetime = gettype($_POST[CoffeeTable::DATE_CREATED->value]) === "integer"
? DateTimeImmutable::createFromTimestamp($_POST[CoffeeTable::DATE_CREATED->value])
: new DateTimeImmutable($_POST[CoffeeTable::DATE_CREATED->value]);
} catch (DateMalformedStringException $error) {
return new Response($error->getMessage(), 400);
}
}
return new Response(Coffee::new($datetime));
}
}

View file

@ -1,8 +0,0 @@
{
"require": {
"reflect/plugin-rules": "^1.5",
"victorwesterlund/xenum": "dev-master",
"vlw/mysql": "dev-master"
},
"minimum-stability": "dev"
}

125
api/composer.lock generated
View file

@ -1,125 +0,0 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f3f2b3cb3bd789eee6af4a93f4a6e0f9",
"packages": [
{
"name": "reflect/plugin-rules",
"version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/VictorWesterlund/reflect-rules-plugin.git",
"reference": "9c837fd1944133edfed70a63ce8b32bf67f0d94b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/VictorWesterlund/reflect-rules-plugin/zipball/9c837fd1944133edfed70a63ce8b32bf67f0d94b",
"reference": "9c837fd1944133edfed70a63ce8b32bf67f0d94b",
"shasum": ""
},
"type": "library",
"autoload": {
"psr-4": {
"ReflectRules\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-3.0-only"
],
"authors": [
{
"name": "Victor Westerlund",
"email": "victor.vesterlund@gmail.com"
}
],
"description": "Add request search paramter and request body constraints to an API built with Reflect",
"support": {
"issues": "https://github.com/VictorWesterlund/reflect-rules-plugin/issues",
"source": "https://github.com/VictorWesterlund/reflect-rules-plugin/tree/1.5.0"
},
"time": "2024-01-17T11:07:44+00:00"
},
{
"name": "victorwesterlund/xenum",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/VictorWesterlund/php-xenum.git",
"reference": "8972f06f42abd1f382807a67e937d5564bb89699"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/VictorWesterlund/php-xenum/zipball/8972f06f42abd1f382807a67e937d5564bb89699",
"reference": "8972f06f42abd1f382807a67e937d5564bb89699",
"shasum": ""
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"victorwesterlund\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-3.0-only"
],
"authors": [
{
"name": "Victor Westerlund",
"email": "victor.vesterlund@gmail.com"
}
],
"description": "PHP eXtended Enums. The missing quality-of-life features from PHP 8+ Enums",
"support": {
"issues": "https://github.com/VictorWesterlund/php-xenum/issues",
"source": "https://github.com/VictorWesterlund/php-xenum/tree/1.1.1"
},
"time": "2023-11-20T10:10:39+00:00"
},
{
"name": "vlw/mysql",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://codeberg.org/vlw/php-mysql",
"reference": "619f43b3bfab9eb034dca3e54c7466055240c861"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"vlw\\MySQL\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-3.0-or-later"
],
"authors": [
{
"name": "Victor Westerlund",
"email": "victor@vlw.se"
}
],
"description": "Abstraction library for common MySQL/MariaDB DML operations with php-mysqli",
"time": "2024-09-25T13:28:15+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
"victorwesterlund/xenum": 20,
"vlw/mysql": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.0.0"
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,51 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Messages\MessagesModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Messages/Messages.php");
class POST_Messages extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(MessagesModel::EMAIL->value))
->type(Type::STRING)
->max(255)
->default(null),
(new Rules(MessagesModel::MESSAGE->value))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_TEXT_MAX_LENGTH)
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
public function main(): Response {
// Use copy of request body as entity
$entity = $_POST;
$entity[MessagesModel::ID->value] = parent::gen_uuid4();
$entity[MessagesModel::DATE_CREATED->value] = time();
return $this->db->for(MessagesModel::TABLE)->insert($entity) === true
? new Response($entity[MessagesModel::ID->value], 201)
: new Response("Failed to create message", 500);
}
}

View file

@ -1,59 +0,0 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
require_once Path::root("src/Endpoints.php");
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/Work.php");
class GET_Search extends VLWdb {
const GET_QUERY = "q";
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(self::GET_QUERY))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
private function search_work(): Response {
return (new Call(Endpoints::WORK->value))->params([
WorkModel::TITLE->value => $_GET[self::GET_QUERY],
WorkModel::SUMMARY->value => $_GET[self::GET_QUERY]
])->get();
}
public function main(): Response {
$results = [
Endpoints::WORK->value => $this->search_work()->output()
];
// 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 404 if no search results
return new Response($results, $num_results > 0 ? 200 : 404);
}
}

View file

@ -1,67 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use const VLW\API\RESP_DELETE_OK;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\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");
class DELETE_Work extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(WorkModel::ID->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkModel::TITLE->value))
->type(Type::STRING)
->min(3)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkModel::SUMMARY->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_TEXT_MAX_LENGTH),
(new Rules(WorkModel::IS_LISTABLE->value))
->type(Type::BOOLEAN),
(new Rules(WorkModel::IS_READABLE->value))
->type(Type::BOOLEAN),
(new Rules(WorkModel::DATE_MODIFIED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH),
(new Rules(WorkModel::DATE_CREATED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH)
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
public function main(): Response {
return $this->db->for(FieldsEnumsModel::TABLE)->delete($_POST) === true
? new Response(RESP_DELETE_OK)
: new Response("Failed to delete work entity", 500);
}
}

View file

@ -1,97 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/Work.php");
class GET_Work extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(WorkModel::ID->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkModel::TITLE->value))
->type(Type::STRING)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkModel::SUMMARY->value))
->type(Type::STRING)
->max(parent::MYSQL_TEXT_MAX_LENGTH),
(new Rules(WorkModel::IS_LISTABLE->value))
->type(Type::BOOLEAN)
->default(true),
(new Rules(WorkModel::IS_READABLE->value))
->type(Type::BOOLEAN)
->default(true),
(new Rules(WorkModel::DATE_MODIFIED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH),
(new Rules(WorkModel::DATE_CREATED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH)
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
public function main(): Response {
// 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]}%"
];
}
// 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]}%"
];
}
$response = $this->db->for(WorkModel::TABLE)
->where($filters)
->select([
WorkModel::ID->value,
WorkModel::TITLE->value,
WorkModel::SUMMARY->value,
WorkModel::IS_LISTABLE->value,
WorkModel::IS_READABLE->value,
WorkModel::DATE_YEAR->value,
WorkModel::DATE_MONTH->value,
WorkModel::DATE_DAY->value,
WorkModel::DATE_MODIFIED->value,
WorkModel::DATE_CREATED->value
]);
return $response->num_rows > 0
? new Response($response->fetch_all(MYSQLI_ASSOC))
: new Response([], 404);
}
}

View file

@ -1,118 +0,0 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\{
WorkModel,
WorkPermalinksModel
};
require_once Path::root("src/Endpoints.php");
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/Work.php");
require_once Path::root("src/databases/models/Work/WorkPermalinks.php");
class PATCH_Work extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(WorkModel::ID->value))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
]);
$this->ruleset->POST([
(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)
->default(time()),
(new Rules(WorkModel::DATE_CREATED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH)
]);
parent::__construct();
}
// Generate a slug URL from string
private static function gen_slug(string $input): string {
return strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $input)));
}
// Compute and return modeled year, month, and day from Unix timestamp in request body
private static function gen_date_created(): array {
return [
WorkModel::DATE_YEAR->value => date("Y", $_POST[WorkModel::DATE_CREATED->value]),
WorkModel::DATE_MONTH ->value => date("n", $_POST[WorkModel::DATE_CREATED->value]),
WorkModel::DATE_DAY->value => date("j", $_POST[WorkModel::DATE_CREATED->value])
];
}
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 {
// Use copy of request body as entity
$entity = $_POST;
// Generate a new slug id from title if changed
if ($_POST[WorkModel::TITLE->value]) {
$slug = $_POST[WorkModel::TITLE->value];
// Bail out if the slug generated from the new tite already exist
if ($this->get_entity_by_id($slug)) {
return new Response("An entity with this title already exist", 409);
}
// Add the new slug to update entity
$entity[WorkModel::ID] = $slug;
}
// Generate new work date fields from timestamp
if ($_POST[WorkModel::DATE_CREATED->value]) {
array_merge($entity, self::gen_date_created());
}
// 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);
}
}

View file

@ -1,114 +0,0 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\{
WorkModel,
WorkPermalinksModel
};
require_once Path::root("src/Endpoints.php");
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/Work.php");
require_once Path::root("src/databases/models/Work/WorkPermalinks.php");
class POST_Work extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(WorkModel::TITLE->value))
->type(Type::STRING)
->min(3)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
->default(null),
(new Rules(WorkModel::SUMMARY->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_TEXT_MAX_LENGTH)
->default(null),
(new Rules(WorkModel::IS_LISTABLE->value))
->type(Type::BOOLEAN)
->default(false),
(new Rules(WorkModel::IS_READABLE->value))
->type(Type::BOOLEAN)
->default(false),
(new Rules(WorkModel::DATE_CREATED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH)
->default(time())
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
// Generate a slug URL from string
private static function gen_slug(string $input): string {
return strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $input)));
}
// Compute and return modeled year, month, and day from a Unix timestamp
private static function gen_date_created(): array {
// Use provided timestamp in request
$date_created = $_POST[WorkModel::DATE_CREATED->value];
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)
];
}
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 {
// Use copy of request body as entity
$entity = $_POST;
// Generate URL slug from title text or UUID if undefined
$entity[WorkModel::ID->value] = $_POST[WorkModel::TITLE->value]
? self::gen_slug($_POST[WorkModel::TITLE->value])
: parent::gen_uuid4();
// 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);
}
// 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);
}
// 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()
]);
}
}

View file

@ -1,38 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use const VLW\API\RESP_DELETE_OK;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/WorkActions.php");
class DELETE_WorkActions extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
parent::__construct();
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(WorkActionsModel::REF_WORK_ID->value))
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
]);
}
public function main(): Response {
return $this->db->for(WorkActionsModel::TABLE)->delete($_POST) === true
? new Response(RESP_DELETE_OK)
: new Response("Failed to delete action for work entity", 500);
}
}

View file

@ -1,49 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\WorkActionsModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/WorkActions.php");
class GET_WorkActions extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->GET([
(new Rules(WorkActionsModel::REF_WORK_ID->value))
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH)
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
public function main(): Response {
$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
]);
return $response->num_rows > 0
? new Response($response->fetch_all(MYSQLI_ASSOC))
: new Response([], 404);
}
}

View file

@ -1,80 +0,0 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\{
WorkModel,
WorkActionsModel
};
require_once Path::root("src/Endpoints.php");
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/Work.php");
require_once Path::root("src/databases/models/Work/WorkActions.php");
class POST_WorkActions extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$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)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkActionsModel::HREF->value))
->required()
->type(Type::STRING)
->type(Type::NULL)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkActionsModel::CLASS_LIST->value))
->type(Type::ARRAY)
->min(1)
->default([]),
(new Rules(WorkActionsModel::EXTERNAL->value))
->type(Type::BOOLEAN)
->default(false)
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
private static function get_entity(): Response {
return (new Call(Endpoints::WORK->value))->params([
WorkModel::ID->value => $_POST[WorkActionsModel::REF_WORK_ID->value]
])->get();
}
public function main(): Response {
// Bail out if work entity could not be fetched
$entity = self::get_entity();
if (!$entity->ok) {
return $entity;
}
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);
}
}

View file

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

View file

@ -1,65 +0,0 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\WorkPermalinksModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/WorkPermalinks.php");
class POST_WorkPermalinks extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(WorkPermalinksModel::ID->value))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkPermalinksModel::REF_WORK_ID->value))
->required()
->type(Type::STRING)
->min(1)
->max(parent::MYSQL_VARCHAR_MAX_LENGTH),
(new Rules(WorkPermalinksModel::DATE_CREATED->value))
->type(Type::NUMBER)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH)
->default(time())
]);
parent::__construct(Databases::VLW, $this->ruleset);
}
private static function get_entity(): Response {
return (new Call(Endpoints::WORK->value))->params([
WorkModel::ID->value => $_POST[WorkTagsModel::REF_WORK_ID->value]
])->get();
}
public function main(): Response {
// Bail out if work entity could not be fetched
$entity = self::get_entity();
if (!$entity->ok) {
return $entity;
}
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);
}
}

View file

@ -1,42 +0,0 @@
<?php
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use const VLW\API\RESP_DELETE_OK;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\WorkTagsModel;
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/WorkTags.php");
class DELETE_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 {
return $this->db->for(WorkTagsModel::TABLE)->delete($_POST) === true
? new Response(RESP_DELETE_OK)
: new Response("Failed to delete value from document", 500);
}
}

View file

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

View file

@ -1,63 +0,0 @@
<?php
use Reflect\Call;
use Reflect\Path;
use Reflect\Response;
use ReflectRules\Type;
use ReflectRules\Rules;
use ReflectRules\Ruleset;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\{
VLWdb,
Databases
};
use VLW\API\Databases\VLWdb\Models\Work\{
WorkModel,
WorkTagsModel,
WorkTagsNameEnum
};
require_once Path::root("src/Endpoints.php");
require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/Work.php");
require_once Path::root("src/databases/models/Work/WorkTags.php");
class POST_WorkTags extends VLWdb {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$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);
}
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 work entity could not be fetched
$entity = self::get_entity();
if (!$entity->ok) {
return $entity;
}
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);
}
}

View file

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

19
api/languages/GET.php Normal file
View file

@ -0,0 +1,19 @@
<?php
use Reflect\{Response, Path};
use VLW\API\API;
use VLW\Database\Models\Languages\Language;
require_once Path::root("src/API/API.php");
require_once Path::root("src/Database/Models/Languages/Language.php");
final class GET_Languages extends API {
public function __construct() {
parent::__construct();
}
public function main(): Response {
return new Response(Language::all());
}
}

View file

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

View file

@ -1,83 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb;
use Reflect\Path;
use Reflect\Request;
use Reflect\Response;
use ReflectRules\Ruleset;
use vlw\MySQL\MySQL;
enum Databases: string {
case VLW = "vlw";
case BATTLESTATION = "battlestation";
}
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_TINYINT_MAX_LENGTH = 255;
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);
// Create new MariaDB connection
$this->db = new MySQL(
$_ENV["connect"]["host"],
$_ENV["connect"]["user"],
$_ENV["connect"]["pass"],
$_ENV["databases"][$database->value],
);
}
// Generate and return UUID4 string
public static function gen_uuid4(): string {
return sprintf("%04x%04x-%04x-%04x-%04x-%04x%04x%04x",
// 32 bits for "time_low"
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
// 16 bits for "time_mid"
mt_rand(0, 0xffff),
// 16 bits for "time_hi_and_version",
// four most significant bits holds version number 4
mt_rand(0, 0x0fff) | 0x4000,
// 16 bits, 8 bits for "clk_seq_hi_res",
// 8 bits for "clk_seq_low",
// two most significant bits holds zero and one for variant DCE1.1
mt_rand(0, 0x3fff) | 0x8000,
// 48 bits for "node"
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
// 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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Work;
enum WorkActionsModel: string {
const TABLE = "work_actions";
case REF_WORK_ID = "ref_work_id";
case DISPLAY_TEXT = "display_text";
case HREF = "href";
case CLASS_LIST = "class_list";
case EXTERNAL = "external";
}

View file

@ -1,10 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Work;
enum WorkMediaModel: string {
const TABLE = "work_media";
case ANCHOR = "anchor";
case MEDIA = "media";
}

View file

@ -1,11 +0,0 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Work;
enum WorkPermalinksModel: string {
const TABLE = "work_permalinks";
case SLUG = "slug";
case ANCHOR = "anchor";
case DATE_TIMESTAMP_CREATED = "date_timestamp_created";
}

View file

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

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

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

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

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

View file

@ -1,88 +0,0 @@
/* # 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);
}
/* # Sections */
/* ## Divider */
vv-shell > hr {
border-color: rgba(255, 255, 255, .1);
}
/* ## About */
section.about {
display: flex;
flex-direction: column;
gap: calc(var(--padding) / 2);
}
section.about p:first-of-type:first-letter {
font-size: 1.8rem;
font-weight: bold;
margin-right: .1rem;
color: var(--color-accent);
}
section.about span.interests {
-webkit-user-select: none;
user-select: none;
color: var(--color-accent);
animation: interests-hue 5s infinite linear;
}
section.about p i:not(:hover) {
opacity: .3;
}
/* # Interests */
div.interests {
--text-shadow-blur: 30px;
transition: 300ms opacity;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
font-weight: bold;
pointer-events: none;
font-size: clamp(16px, 15vw, 50px);
color: var(--color-accent);
overflow: hidden;
opacity: 0;
z-index: 200;
}
div.interests.active {
opacity: 1;
}
div.interests p {
transition: 500ms transform cubic-bezier(.34,0,0,.99);
position: absolute;
text-shadow:
0 0 var(--text-shadow-blur) black,
0 0 var(--text-shadow-blur) black,
0 0 var(--text-shadow-blur) black,
0 0 var(--text-shadow-blur) black,
0 0 var(--text-shadow-blur) black;
}
@keyframes interests-hue {
to {
-webkit-filter: hue-rotate(360deg);
filter: hue-rotate(360deg);
}
}

View file

@ -1,45 +0,0 @@
/* # 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;
}
}

View file

@ -1,290 +0,0 @@
/* # 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;
}
}

View file

@ -1,85 +0,0 @@
/* # Overrides */
[vv-page="/search"]:not(body) {
display: flex;
flex-direction: column;
gap: var(--padding);
}
/* # Sections */
/* ## Search */
section.search {
width: 100%;
display: none;
flex-direction: column;
align-items: center;
gap: var(--padding);
background-color: rgba(255, 255, 255, .05);
padding: calc(var(--padding) * 1.5);
margin-bottom: calc(var(--padding) * 2);
}
vv-shell[vv-page="/search"] > section.search {
display: flex;
}
section.search form {
display: contents;
}
section.search search {
width: 100%;
}
section.search input {
width: 100%;
border: none;
color: black;
outline: none;
padding: var(--padding);
background-color: var(--color-accent);
}
section.search button[type="submit"] {
width: 100%;
max-width: 350px;
}
section.search > svg {
width: 100%;
}
/* # Search results */
section.results .result {
display: flex;
flex-direction: column;
gap: calc(var(--padding) / 2);
}
/* ## Titles */
section.title a h2 {
color: var(--color-accent);
}
section.title a h2::before {
content: "// ";
color: white;
}
/* ## Work */
section.results.work {
display: grid;
grid-template-columns: 1fr;
gap: calc(var(--padding) / 2);
}
section.results.work .result {
padding: var(--padding);
background-color: rgba(255, 255, 255, .03);
border-radius: 6px;
}

View file

@ -1,72 +0,0 @@
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 });
});
});

View file

@ -1,50 +0,0 @@
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);
});
}

View file

@ -1,173 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 19 KiB

View file

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,7 +1,7 @@
{
"require": {
"reflect/client": "dev-master",
"victorwesterlund/xenum": "dev-master"
"vlw/mysql": "dev-master",
"vlw/xenum": "dev-master"
},
"minimum-stability": "dev"
}

56
composer.lock generated
View file

@ -4,65 +4,49 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "2a8a06dc452a4eb9055d238f771dcd37",
"content-hash": "a7ce20d192550ef2d037220b593b5eb9",
"packages": [
{
"name": "reflect/client",
"name": "vlw/mysql",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/VictorWesterlund/reflect-client-php.git",
"reference": "89a8c041044c8c60cefafc4716d5d61b96c43e06"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/VictorWesterlund/reflect-client-php/zipball/89a8c041044c8c60cefafc4716d5d61b96c43e06",
"reference": "89a8c041044c8c60cefafc4716d5d61b96c43e06",
"shasum": ""
"url": "https://codeberg.org/vlw/php-mysql",
"reference": "0e367f797fa9348408881ed758976f21e8c667e4"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"Reflect\\": "src/Reflect/"
"vlw\\MySQL\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-only"
"GPL-3.0-or-later"
],
"authors": [
{
"name": "Victor Westerlund",
"email": "victor.vesterlund@gmail.com"
"email": "victor@vlw.se"
}
],
"description": "Extendable PHP interface for communicating with Reflect API over HTTP or UNIX sockets",
"support": {
"issues": "https://github.com/VictorWesterlund/reflect-client-php/issues",
"source": "https://github.com/VictorWesterlund/reflect-client-php/tree/3.0.6"
},
"time": "2024-04-06T14:55:04+00:00"
"description": "Abstraction library for common MySQL/MariaDB DML operations with php-mysqli",
"time": "2025-07-29T07:46:46+00:00"
},
{
"name": "victorwesterlund/xenum",
"name": "vlw/xenum",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/VictorWesterlund/php-xenum.git",
"reference": "8972f06f42abd1f382807a67e937d5564bb89699"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/VictorWesterlund/php-xenum/zipball/8972f06f42abd1f382807a67e937d5564bb89699",
"reference": "8972f06f42abd1f382807a67e937d5564bb89699",
"shasum": ""
"url": "https://codeberg.org/vlw/php-xenum",
"reference": "ba3f43a9e2787bf938cfbfcb85ea87e5062df294"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"victorwesterlund\\": "src/"
"vlw\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@ -72,27 +56,23 @@
"authors": [
{
"name": "Victor Westerlund",
"email": "victor.vesterlund@gmail.com"
"email": "victor@vlw.se"
}
],
"description": "PHP eXtended Enums. The missing quality-of-life features from PHP 8+ Enums",
"support": {
"issues": "https://github.com/VictorWesterlund/php-xenum/issues",
"source": "https://github.com/VictorWesterlund/php-xenum/tree/1.1.1"
},
"time": "2023-11-20T10:10:39+00:00"
"time": "2025-05-10T11:28:03+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
"reflect/client": 20,
"victorwesterlund/xenum": 20
"vlw/mysql": 20,
"vlw/xenum": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.0.0"
"plugin-api-version": "2.3.0"
}

View file

@ -1,10 +1,251 @@
# Install dependencies
composer install --optimize-autoloader
npm install
#!/bin/bash
# (Re)create public NPM modules folder
rm -r assets/js/modules/npm
mkdir assets/js/modules/npm
# Define constants
DB_VLW="vlw"
DB_API="vlw_api"
# Create link to Elevent MJS from public JS modules folder
ln -sr node_modules/elevent/src/Elevent.mjs assets/js/modules/npm/Elevent.mjs
# Initialize variables
cwd=""
database_app_host=""
database_app_user=""
database_seed_host=""
database_seed_user=""
database_app_password=""
database_seed_password=""
echo_err() {
echo "!! -> $1"
}
# Make sure we have all the system packages we need to proceed with the installation
check_sys_depend() {
local valid=true
local packages=("git" "composer")
for package in "${packages[@]}" ; do
if ! dpkg -l | grep -qw "ii ${package}" ; then
echo_err "Package '${package}' is not installed."
valid=false
fi
done
# Bail out if any required package is missing
if [ "$valid" = false ] ; then
exit 1
fi
}
install_vegvisir() {
echo
echo "Installing Vegvisir into '$cwd/vegvisir'"
curl -fsSL https://codeberg.org/vegvisir/install/raw/branch/master/install.sh | bash -s -- --install=n --overwrite=y --example=n --dir="$cwd"
}
install_reflect() {
echo
echo "Installing Reflect into '$cwd/reflect'"
curl -fsSL https://codeberg.org/reflect/install/raw/branch/master/install.sh | bash -s -- --install=n --overwrite=y --seed=n --dir="$cwd" --endpoints="api" --host="$database_app_host" --user="$database_app_user" --password="$database_app_password" --db="$DB_API"
}
install_vlw() {
composer install --classmap-authoritative
}
seed_databases() {
echo
echo "-- Database seeding --"
echo "We're now going to seed the databases '$DB_VLW' and '$DB_API' with default data"
echo "- Make sure that both databases exist and are empty"
echo "- Your credentials for this user '$(whoami)' might be different from your app credentials (php-mysql)"
echo
# Database seed hostname
echo "Enter the full hostname of your MySQL/MariaDB server to use in this script for seeding."
read -p "Press enter to use the same host as the app credentials [$database_app_host]: " database_seed_host </dev/tty
# Use the same database host as the app configuration
if [[ "$database_seed_host" == "" ]] ; then
database_seed_host=$database_app_host
fi
# Database seed user
echo
echo "Enter the username for your MySQL/MariaDB server to use in this script for seeding."
read -p "Press enter to use the same host as the app credentials [$database_app_user]: " database_seed_user </dev/tty
# Use the same database user as the app configuration
if [[ "$database_seed_user" == "" ]] ; then
database_seed_user=$database_app_user
fi
# Database seed password
echo
echo "Enter the password for your MySQL/MariaDB server to use in this script for seeding."
echo "Enter 'null' to disable password authentication"
read -p "Press enter to use the same password as the app credentials [<database_password>]: " database_seed_password </dev/tty
# Use the same database password as the app configuration
if [[ "$database_seed_password" == "" ]] ; then
database_seed_password=$database_app_password
fi
# Seed the main database
mysql -h "$database_seed_host" -u "$database_seed_user" --password="$database_seed_password" $DB_VLW < src/Database/Seeds/vlw.sql
if [ $? -ne 0 ]; then
echo_err "Installation aborted: MySQL error"
exit 1
fi
# Seed the API database
mysql -h "$database_seed_host" -u "$database_seed_user" --password="$database_seed_password" $DB_API < src/Database/Seeds/api.sql
if [ $? -ne 0 ]; then
echo_err "Installation aborted: MySQL error"
exit 1
fi
}
configure_vlw() {
local config_password=""
local config_available=""
local config_available_to=""
local config_available_from=""
local config_available_average=""
local config_available_timezone=""
local config_forgejo=""
local config_forgejo_url=""
local config_forgejo_profiles=""
# A configuration file already exists
if [[ -f ".env.ini" ]] ; then
echo
echo "A configuration file already exists at: ${cwd}/.env.ini"
read -p "Do you want to overwrite this file? (y/n): " overwrite </dev/tty
# Remove existing configuration file or abort
if [[ "$overwrite" == "y" || "$overwrite" == "Y" ]] ; then
echo "Removing existing configuration and proceeding with the installation in ${cwd}..."
rm .env.ini
else
echo_err "Installation aborted: Configuration file already exists"
exit 1
fi
fi
echo
echo "-- Database configuration --"
read -p "Enter the full hostname of your MySQL/MariaDB server (php-mysql): " database_app_host </dev/tty
read -p "Enter the app username for your MySQL/MariaDB server (php-mysql): " database_app_user </dev/tty
read -p "Enter the app password for your MySQL/MariaDB server (php-mysql): " config_password </dev/tty
# Coerce empty input as null keyword for later configurations
if [[ "$config_password" == "" ]] ; then
database_app_password="null"
fi
echo
read -p "(Optional) Would you like to configure time available settings? (y/n): " config_available </dev/tty
# Check the user's response
if [[ "$config_available" == "n" || "$config_available" == "N" ]]; then
config_available_to=0
config_available_from=0
config_available_average=0
config_available_timezone="Europe/Stockholm"
fi
if ! [[ -n "$config_available_timezone" ]]; then
read -p "Enter your timezone in IANA Time Zone Database Format ('Europe/Stockholm' for example): " config_available_timezone </dev/tty
fi
if ! [[ -n "$config_available_from" ]]; then
read -p "Enter time available from hour (24-hour format): " config_available_from </dev/tty
fi
if ! [[ -n "$config_available_to" ]]; then
read -p "Enter time available to hour (24-hour format): " config_available_to </dev/tty
fi
if ! [[ -n "$config_available_average" ]]; then
read -p "Enter average reply time in hours: " config_available_average </dev/tty
fi
echo
read -p "(Optional) Would you like to configure Forgejo language updates? (y/n): " config_forgejo </dev/tty
# Check the user's response
if [[ "$config_forgejo" == "n" || "$config_forgejo" == "N" ]]; then
config_forgejo_url="https://git.vlw.se"
config_forgejo_profiles="vlw,vegvisir,reflect"
fi
if ! [[ -n "$config_forgejo_url" ]]; then
read -p "Enter a hostname to a Forgejo instance ('https://git.vlw.se' for example): " config_forgejo_url </dev/tty
fi
if ! [[ -n "$config_forgejo_profiles" ]]; then
read -p "Enter a comma seperated list of Forgejo profiles to scan ('vlw,vegvisir,reflect' for example): " config_forgejo_profiles </dev/tty
fi
local config=(
"; This config file was generated automatically by ./install.sh"
"; Refer to '.env.example.ini' for more information"
"[mariadb]"
"host = '$database_app_host'"
"user = '$database_app_user'"
"pass = '$config_password'"
"db = '$DB_VLW'"
"[config_time_available]"
"time_zone = '$config_available_timezone'"
"available_to_hour = '$config_available_to'"
"reply_average_hours = '$config_available_average'"
"available_from_hour = '$config_available_from'"
"[service_forgejo]"
"url = '$config_forgejo_url'"
"profiles = '$config_forgejo_profiles'"
)
for line in "${config[@]}" ; do
echo "${line}" >> .env.ini
done
}
main() {
# Get the current working directory
cwd=$(pwd)
check_sys_depend
configure_vlw
seed_databases
install_vlw
install_vegvisir
install_reflect
echo "-- Success --"
echo "vlw.se has been installed! :)"
echo "- Point all traffic to your web server to '${cwd}/vegvisir/public/index.php'"
echo "- Point all traffic to your REST API server to '${cwd}/reflect/public/index.php'"
echo "-------------"
echo
}
# Prompt the user for confirmation
echo
echo "-- Installing vlw.se --"
echo "You are currently in: $(pwd)"
read -p "Do you want to proceed with the installation in this directory? (y/n): " choice </dev/tty
# Check the user's response
if [[ "$choice" == "y" || "$choice" == "Y" ]] ; then
echo "Proceeding with the installation in $(pwd)..."
main
else
echo "Installation aborted."
fi

17
package-lock.json generated
View file

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

View file

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

View file

@ -1,43 +1,123 @@
<style><?= VV::css("assets/css/pages/about") ?></style>
<?php
use VLW\Database\Models\Coffee\Coffee;
use VLW\Database\Models\Languages\Language;
require_once VV::root("src/Database/Models/Coffee/Coffee.php");
require_once VV::root("src/Database/Models/Languages/Language.php");
const FORGEJO = "https://git.vlw.se/explore/repos?language=";
const SI_BYTE_MULTIPLE = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
$languages = new class extends Language {
private readonly int $total_bytes;
public function __construct() {
$this->total_bytes = array_sum(array_map(fn(Language $language): int => $language->bytes, parent::all()));
}
public function percent(Language $language, int $mode = PHP_ROUND_HALF_UP): int {
return round(($language->bytes / $this->total_bytes) * 100, 0, $mode);
}
public function percent_string(Language $language): string {
return ($this->percent($language) > 1 ? $this->percent($language) : "<1") . "%";
}
public function bytes_si_string(Language $language): string {
// Calculate factor for unit
$factor = floor((strlen($language->bytes) - 1) / 3);
// Divide by radix 10
$format = $language->bytes / pow(1000, $factor);
return round($format) . " " . SI_BYTE_MULTIPLE[$factor];
}
};
$coffee = new class extends Coffee {
public readonly int $count_week;
public readonly int $count_week_average;
public function __construct() {
$this->count_week = parent::count_week();
$this->count_week_average = parent::count_week_average();
}
public function week_average_string(): string {
$diff = $this->count_week - $this->count_week_average;
return match (true) {
$diff < 0 => "less than",
$diff === 0 => "the same as",
$diff > 0 => "more than"
};
}
};
?>
<style><?= VV::css("public/assets/css/pages/about") ?></style>
<section class="intro">
<h2 aria-hidden="true">Hi, I'm</h2>
<h1>Victor Westerlund</h1>
</section>
<hr aria-hidden="true">
<section class="about">
<p>I&ZeroWidthSpace;'m a full-stack web developer from Sweden.</p>
<p>The &lt;programming/markup/command/query/whatever&gt;-languages I currently use the most are (in a mostly accurate decending order): PHP, JavaScript, CSS, MySQL, TypeScript, Python, SQLite, Bash, and HTML.</p>
<p>I&ZeroWidthSpace;'m a full-stack web developer from Sweden, and welcome to my little personal corner of the Internet!</p>
<p>My coding happens almost exclusivly in <a href="https://github.com/coder/code-server">code-server</a>, which is a fork of VSCode that runs entirely in the browser. I keep my development environment tucked away in a lightweight Debian VA that I can tote around to whatever host machine I happen to work on. I also keep an ephemeral Debian Live ISO ready which boots into a VM RAM disk where I can mess around without fear or breaking things or try new software.</p>
<p>I used to list the &lt;programming/markup/command/whatever&gt;-languages here that I use the most and order them by guesstimating how much I use each one. But then I thought it would be better to just show you instead using this chart that automatically pulls the total bytes for each language from my <a href="https://git.vlw.se/explore/repos">public repos on Forgejo</a>.</p>
</section>
<section class="languages">
<stacked-bar-chart>
<?php foreach ($languages::all() as $language): ?>
<a href="<?= FORGEJO . $language->name ?>" target="_blank"><chart-segment style="--size:<?= $languages->percent($language) ?>%;" data-lang="<?= $language->name ?>" data-bytes="<?= $language->bytes ?>">
<span data-hover><strong><?= $languages->percent_string($language) ?> <?= $language->name ?></strong><br>(<?= $language->bytes ?> bytes)</span>
</chart-segment></a>
<?php endforeach; ?>
</stacked-bar-chart>
<languages-list>
<?php foreach ($languages::all() as $language): ?>
<a href="<?= FORGEJO . $language->name ?>"><button data-lang="<?= $language->name ?>" class="inline">
<p><?= $languages->percent_string($language) ?></p>
<p class="lang"><?= $language->name ?></p>
<p><?= $languages->bytes_si_string($language) ?></p>
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
</button></a>
<?php endforeach; ?>
</languages-list>
<stacked-bar-chart>
<?php foreach ($languages::all() as $language): ?>
<a href="<?= FORGEJO . $language->name ?>" target="_blank"><chart-segment style="--size:<?= $languages->percent($language) ?>%;" data-lang="<?= $language->name ?>" data-bytes="<?= $language->bytes ?>">
<span data-hover><strong><?= $languages->percent_string($language) ?> <?= $language->name ?></strong><br>(<?= $language->bytes ?> bytes)</span>
</chart-segment></a>
<?php endforeach; ?>
</stacked-bar-chart>
</section>
<section class="about">
<h2>This website</h2>
<p>This site and all of its components are <a href="https://codeberg.org/vlw/vlw.se">100% free and open source software</a>. The website is designed and built by me from the ground up using my <a href="https://vegvisir.vlw.se">web</a> and <a href="https://reflect.vlw.se">API</a> frameworks as the foundation. You will find no cookies or trackers here. The only information I have about you is your public IP-address and which resources on this site your browser requests. None of this data is used for any kind of analytics.</p>
<p><a href="https://srv.vlw.se"><i>See detailed information about all servers/services on this domain</i></a></p>
<p>This site and all of its components, including texts and graphics have been created by me and are all <a href="https://codeberg.org/vlw/vlw.se">100% free and open source</a>. Feel free to use anything you see on this website in your own projects as long as it's under the same GNU GPLv3-or-later license. The website is designed by me on top of my own <a href="https://vegvisir.vlw.se">web framework</a> and <a href="https://reflect.vlw.se">API framework</a>.</p>
<p>You won't find any cookies or trackers on this site! The only information I have about you are in the standard NGINX access and error logs, which get overwritten automatically after some time.</p>
</section>
<section class="about">
<h2>Personal</h2>
<p>Coffee, of course.. and..</p>
<p>At times, I become a true, amateur, armchair detective for a <span class="interests">variety of your typical-nerdy topics that I find interesting</span>. And will spend a disproportionate to real-world-personal-use amount of time reading about that stuff too.</p>
<p>Another silent passion of mine that comes out every few years is <a href="/about/battlestation" vv="about" vv-call="navigate">building computers</a> and fiddling with weird networking stuff.</p>
<p>And then of course I don't mind some occational gaming, and watching movies and TV-series.</p>
</section>
<section class="about">
<h2>Projects</h2>
<p>Here are some projects I'm working on right now:</p>
<p>* <a href="https://vegvisir.vlw.se">Vegvisir</a>: A web navigation framework for PHP.</p>
<p>* <a href="https://reflect.vlw.se">Reflect</a>: A REST API framework for PHP developers.</p>
<p>There is more stuff on my <a href="work" vv="about" vv-call="navigate">works page</a> and even more stuff on <a href="https://codeberg.org/vlw">my Codeberg profile</a>.</p>
<p><a href="https://git.vlw.se/vlw"><i>and even EVEN more stuff on my Forgejo</i></a></p>
<p>One thing that most people know about me is that I like coffee.. lots of coffee. In fact, I've had <?= $coffee->count_week ?> cup<?= $coffee->count_week === 1 ? "" : "s" ?> of coffee in the last 7 days! That's <?= $coffee->week_average_string() ?> my average of <?= $coffee->count_week_average ?> per week, impressive! Even though you just read that.. I don't consider myself <i>too much</i> of a coffee snob! As long as it's dark roast and warm, I'm probably happy to have it.</p>
<p>At times, I become a true, amateur, armchair detective for a <span class="interests">variety of your typical-nerdy topics that I find interesting</span> and you can bet I spend way more time reading about those things than I will ever have use for in life.</p>
<p>Another silent passion of mine that comes out every few years is building computers and fiddling with networking stuff.</p>
</section>
<hr>
<section class="about">
<h3>GitHub</h3>
<p>I have <a href="https://giveupgithub.com" target="_blank" rel="noopener noreferer">given up GitHub</a> for their increasing number of injustices againts its users, last betrayal being GitHub's for-profit "Copilot" product which was illegaly trained on copylefted software on its platform.</p>
<p>I signed up and started using GitHub before I became aware of how opressive to to its users and deceptive their business model is. I wasn't aware of the situation.</p>
<p>While I am a bit skeptical to do this in case history repeats itself; [most of] <a href="https://codeberg.org/vlw">my work is now on Codeberg</a> instead. Unfortunately some things like old pull-requests, issues, and branch archives can not be migrated completely.</p>
<p>I have <a href="https://giveupgithub.com" target="_blank" rel="noopener noreferer">given up GitHub</a> and moved most of my free software to <a href="https://codeberg.org/vlw">Codeberg</a>. You can still find my <a href="https://github.com/VictorWesterlund">GitHub profile here</a> but I don't use it for source control of my projects anymore.</p>
</section>
<hr>
<section>
<p>Let's work on something together or just have a chat? <a href="contact" vv="about" vv-call="navigate">Write me a line!</a></p>
<p>Let's work on something together or just have a chat? <a href="contact">Write me a line!</a></p>
</section>
<div class="interests" aria-hidden="true">
@ -54,6 +134,7 @@
<p>engineering</p>
<p>photography</p>
<p>videography</p>
<p>ISO&nbsp;8601</p>
<p>RFC&nbsp;3339</p>
<p>digital archiving</p>
</div>
<script type="module"><?= VV::js("assets/js/pages/about") ?></script>
<script><?= VV::js("public/assets/js/pages/about") ?></script>

View file

@ -1,36 +0,0 @@
<style><?= VV::css("assets/css/pages/about/battlestation-retired") ?></style>
<section class="title">
<h1>Retired components</h1>
<p>I'd be happy to send you any component that you find here for "free". The only thing I ask in return is that you pay for shipping.</p>
<p>This page is still a work-in-progress. You can use my API to get a list of retired components by hardware category for now.</p>
</section>
<section class="actions">
<a href="<?= $_ENV["api"]["base_url"] ?>battlestation/chassis?is_retired=true" target="_blank">
<button class="inline">Cases (API)</button>
</a>
<a href="<?= $_ENV["api"]["base_url"] ?>battlestation/cpu?is_retired=true" target="_blank">
<button class="inline">CPUs (API)</button>
</a>
<a href="<?= $_ENV["api"]["base_url"] ?>battlestation/gpu?is_retired=true" target="_blank">
<button class="inline">GPUs (API)</button>
</a>
<a href="<?= $_ENV["api"]["base_url"] ?>battlestation/mb?is_retired=true" target="_blank">
<button class="inline">Motherboards (API)</button>
</a>
<a href="<?= $_ENV["api"]["base_url"] ?>battlestation/psu?is_retired=true" target="_blank">
<button class="inline">PSUs (API)</button>
</a>
<a href="<?= $_ENV["api"]["base_url"] ?>battlestation/storage?is_retired=true" target="_blank">
<button class="inline">Storage (API)</button>
</a>
</section>
<section class="title">
<h2>Found something you like?</h2>
<p>Please note; I can't guarantee the thing you want will work as expected, or work at all! But I will test the compontent for you if I still have means at hand to do so.</p>
</section>
<section class="actions">
<a href="/contact" vv="battlestation-retired" vv-call="navigate">
<button class="inline solid">Contact me</button>
</a>
</section>
<script><?= VV::js("assets/js/pages/about/battlestation-retired") ?></script>

View file

@ -1,496 +0,0 @@
<?php
use Vegvisir\Path;
use VLW\Client\API;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\Models\Battlestation\{
MbModel,
CpuModel,
GpuModel,
PsuModel,
DramModel,
StorageModel,
ChassisModel
};
use VLW\API\Databases\VLWdb\Models\Battlestation\Config\{
MbPsuModel,
MbGpuModel,
MbDramModel,
ConfigModel,
MbStorageModel,
ChassisMbModel,
MbCpuCoolerModel,
MbStorageSlotFormfactorEnum
};
require_once VV::root("src/client/API.php");
require_once VV::root("api/src/Endpoints.php");
// Load hardware database models
require_once VV::root("api/src/databases/models/Battlestation/Mb.php");
require_once VV::root("api/src/databases/models/Battlestation/Cpu.php");
require_once VV::root("api/src/databases/models/Battlestation/Gpu.php");
require_once VV::root("api/src/databases/models/Battlestation/Psu.php");
require_once VV::root("api/src/databases/models/Battlestation/Dram.php");
require_once VV::root("api/src/databases/models/Battlestation/Storage.php");
require_once VV::root("api/src/databases/models/Battlestation/Chassis.php");
// Load hardware config database models
require_once VV::root("api/src/databases/models/Battlestation/Config/MbPsu.php");
require_once VV::root("api/src/databases/models/Battlestation/Config/MbGpu.php");
require_once VV::root("api/src/databases/models/Battlestation/Config/MbDram.php");
require_once VV::root("api/src/databases/models/Battlestation/Config/Config.php");
require_once VV::root("api/src/databases/models/Battlestation/Config/MbStorage.php");
require_once VV::root("api/src/databases/models/Battlestation/Config/ChassisMb.php");
require_once VV::root("api/src/databases/models/Battlestation/Config/MbCpuCooler.php");
const GIGA = 0x3B9ACA00;
const MEGA = 0xF4240;
// Connect to VLW API
$api = new API();
$config = $api->call(Endpoints::BATTLESTATION->value)->get();
?>
<style><?= VV::css("assets/css/pages/about/battlestation") ?></style>
<?php if ($config->ok): ?>
<section class="title">
<h1>Battle&shy;stations</h1>
<p>I'd be happy to send you, dear reader, any component that you find here for "free" that hasn't been retired yet. The only thing I ask in return is that you pay for shipping.</p>
<p>I can't guarantee the thing you want will work as expected, or work at all! But I will test the compontent for you if I still have means at hand to do so.</p>
<div>
<a href="/about/battlestation-retired" vv="battlestation" vv-call="navigate"><button class="inline solid">Retired components</button></a>
<a href="/contact" vv="battlestation" vv-call="navigate"><button class="inline">Contact me</button></a>
</div>
</section>
<?php foreach ($config->json() as $config): ?>
<?php
// Get motherboard details by ref_mb_id from config
$motherboard = $api->call(Endpoints::BATTLESTATION_MB->value)->params([
MbModel::ID->value => $config[ChassisMbModel::REF_MB_ID->value]
])->get()->json()[0];
?>
<section class="heading">
<h1><?= $config[ConfigModel::FRIENDLY_NAME->value] ?? "Lucious" ?></h1>
<p>This rig was built: <?= date(API::DATE_FORMAT, $config[ConfigModel::DATE_BUILT->value]) ?></p>
</section>
<section class="config"
data-mb="1"
data-cpu="<?= count($motherboard["cpus"]) ?>"
data-psu="<?= count($motherboard["psus"]) ?>"
data-gpu="<?= count($motherboard["gpus"]) ?>"
data-dram="<?= count($motherboard["dram"]) ?>"
data-case="<?= count($motherboard["chassis"]) ?>"
data-drives-mdottwo="<?= count(array_keys(array_column($motherboard["storage"], MbStorageModel::SLOT_FORMFACTOR->value), MbStorageSlotFormfactorEnum::MDOTTWO->value)) ?>"
data-drives-twodotfive="<?= count(array_keys(array_column($motherboard["storage"], MbStorageModel::SLOT_FORMFACTOR->value), MbStorageSlotFormfactorEnum::TWODOTFIVE->value)) ?>"
data-drives-threedotfive="<?= count(array_keys(array_column($motherboard["storage"], MbStorageModel::SLOT_FORMFACTOR->value), MbStorageSlotFormfactorEnum::THREEDOTFIVE->value)) ?>"
>
<?= VV::embed("assets/media/battlestation.svg") ?>
<div class="specs">
<?php // Show motherboard details ?>
<?php if ($motherboard): ?>
<div data-target="mb" class="spec">
<p>Motherboard</p>
<h3><?= $motherboard[MbModel::VENDOR_NAME->value] ?> <span><?= $motherboard[MbModel::VENDOR_MODEL->value] ?></span></h3>
<div>
<div>
<label>Formfactor</label>
<p><?= $motherboard[MbModel::FORMFACTOR->value] ?></p>
</div>
<div>
<label>Brand name</label>
<p><?= $motherboard[MbModel::VENDOR_NAME->value] ?></p>
</div>
<div>
<label>Brand model</label>
<p><?= $motherboard[MbModel::VENDOR_MODEL->value] ?></p>
</div>
<div>
<label>LAN</label>
<p><?= $motherboard[MbModel::NETWORK_ETHERNET->value] ?? "No LAN" ?></p>
</div>
<div>
<label>WLAN</label>
<p><?= $motherboard[MbModel::NETWORK_WLAN->value] ?? "No WLAN" ?></p>
</div>
<div>
<label>Bluetooth</label>
<p><?= $motherboard[MbModel::NETWORK_BLUETOOTH->value] ?? "No Bluetooth" ?></p>
</div>
<div>
<label>Aquired</label>
<p><?= date(API::DATE_FORMAT, $motherboard[MbModel::DATE_AQUIRED->value]) ?></p>
</div>
<?php if ($motherboard[MbModel::IS_RETIRED->value]): ?>
<div>
<label>Retired</label>
<p>Yes</p>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php // List all cases (lol) ?>
<?php foreach ($motherboard["chassis"] as $mb_chassis): ?>
<?php // Get case details from endpoint by id ?>
<?php $case = $api->call(Endpoints::BATTLESTATION_CHASSIS->value)->params([
ChassisModel::ID->value => $mb_chassis[ChassisMbModel::REF_CHASSIS_ID->value]
])->get()->json()[0]; ?>
<div data-target="case" class="spec">
<p>Case</p>
<h3><?= $case[ChassisModel::VENDOR_NAME->value] ?> <span><?= $case[ChassisModel::VENDOR_MODEL->value] ?></span></h3>
<div>
<div>
<label>Brand name</label>
<p><?= $case[ChassisModel::VENDOR_NAME->value] ?></p>
</div>
<div>
<label>Brand model</label>
<p><?= $case[ChassisModel::VENDOR_MODEL->value] ?></p>
</div>
<div>
<label> 2.5" slots</label>
<p><?= $case[ChassisModel::STORAGE_TWOINCHFIVE->value] ?></p>
</div>
<div>
<label> 3.5" slots</label>
<p><?= $case[ChassisModel::STORAGE_THREEINCHFIVE->value] ?></p>
</div>
<div>
<label>Aquired</label>
<p><?= date(API::DATE_FORMAT, $case[ChassisModel::DATE_AQUIRED->value]) ?></p>
</div>
<?php if ($case[ChassisModel::IS_RETIRED->value]): ?>
<div>
<label>Retired</label>
<p>Yes</p>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php // List all CPUs ?>
<?php foreach ($motherboard["cpus"] as $mb_cpu): ?>
<?php // Get case details from endpoint by id ?>
<?php $cpu = $api->call(Endpoints::BATTLESTATION_CPU->value)->params([
CpuModel::ID->value => $mb_cpu[MbCpuCoolerModel::REF_CPU_ID->value]
])->get()->json()[0]; ?>
<div data-target="cpu" class="spec">
<p>CPU</p>
<h3><?= $cpu[CpuModel::VENDOR_NAME->value] ?> <span><?= $cpu[CpuModel::VENDOR_MODEL->value] ?></span></h3>
<div>
<div>
<label>Brand name</label>
<p><?= $cpu[CpuModel::VENDOR_NAME->value] ?></p>
</div>
<div>
<label>Brand model</label>
<p><?= $cpu[CpuModel::VENDOR_MODEL->value] ?></p>
</div>
<div>
<label>Class</label>
<p><?= $cpu[CpuModel::CPU_CLASS->value] ?></p>
</div>
<div>
<label>Base Clockspeed</label>
<p><?= $cpu[CpuModel::CLOCK_BASE->value] / GIGA ?>GHz</p>
</div>
<div>
<label>Turbo Clockspeed</label>
<p><?= $cpu[CpuModel::CLOCK_TURBO->value] / GIGA ?>GHz</p>
</div>
<div>
<label> cores (P/E)</label>
<p><?= $cpu[CpuModel::CORE_COUNT_PERFORMANCE->value] + $cpu[CpuModel::CORE_COUNT_EFFICIENCY->value] ?> (<?= $cpu[CpuModel::CORE_COUNT_PERFORMANCE->value] ?>/<?= $cpu[CpuModel::CORE_COUNT_EFFICIENCY->value] ?>)</p>
</div>
<div>
<label> total threads</label>
<p><?= $cpu[CpuModel::CORE_THREADS->value] ?></p>
</div>
<div>
<label>Aquired</label>
<p><?= date(API::DATE_FORMAT, $cpu[CpuModel::DATE_AQUIRED->value]) ?></p>
</div>
<?php if ($cpu[CpuModel::IS_RETIRED->value]): ?>
<div>
<label>Retired</label>
<p>Yes</p>
</div>
<?php endif; ?>
</div>
<div>
<div>
<label>In motherboard slot number</label>
<p><?= $mb_cpu[MbCpuCoolerModel::SOCKET->value] ?></p>
</div>
<div>
<label>Motherboard slot type</label>
<p><?= $mb_cpu[MbCpuCoolerModel::SOCKET_TYPE->value] ?></p>
</div>
</div>
</div>
<?php endforeach; ?>
<?php // List all GPUs ?>
<?php foreach ($motherboard["gpus"] as $mb_gpu): ?>
<?php // Get case details from endpoint by id ?>
<?php $gpu = $api->call(Endpoints::BATTLESTATION_GPU->value)->params([
GpuModel::ID->value => $mb_gpu[MbGpuModel::REF_GPU_ID->value]
])->get()->json()[0]; ?>
<div data-target="gpu" class="spec">
<p>GPU</p>
<h3><?= $gpu[GpuModel::VENDOR_NAME->value] ?> <span><?= $gpu[GpuModel::VENDOR_CHIP_MODEL->value] ?></span></h3>
<div>
<div>
<label>Chip brand name</label>
<p><?= $gpu[GpuModel::VENDOR_CHIP_NAME->value] ?></p>
</div>
<div>
<label>Chip brand model</label>
<p><?= $gpu[GpuModel::VENDOR_CHIP_MODEL->value] ?></p>
</div>
<div>
<label>VRAM</label>
<p><?= $gpu[GpuModel::MEMORY->value] / GIGA ?>GB</p>
</div>
<div>
<label>Brand name</label>
<p><?= $gpu[GpuModel::VENDOR_NAME->value] ?></p>
</div>
<div>
<label>Brand model</label>
<p><?= $gpu[GpuModel::VENDOR_MODEL->value] ?></p>
</div>
<div>
<label>Aquired</label>
<p><?= date(API::DATE_FORMAT, $gpu[GpuModel::DATE_AQUIRED->value]) ?></p>
</div>
<?php if ($gpu[GpuModel::IS_RETIRED->value]): ?>
<div>
<label>Retired</label>
<p>Yes</p>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php // List all PSUs ?>
<?php foreach ($motherboard["psus"] as $mb_psu): ?>
<?php // Get case details from endpoint by id ?>
<?php $psu = $api->call(Endpoints::BATTLESTATION_PSU->value)->params([
PsuModel::ID->value => $mb_psu[MbPsuModel::REF_PSU_ID->value]
])->get()->json()[0]; ?>
<div data-target="psu" class="spec">
<p>PSU</p>
<h3><?= $psu[PsuModel::VENDOR_NAME->value] ?> <span><?= $psu[PsuModel::VENDOR_MODEL->value] ?></span> <span><?= $psu[PsuModel::POWER->value] ?>W</span></h3>
<div>
<div>
<label>Power</label>
<p><?= $psu[PsuModel::POWER->value] ?>W</p>
</div>
<div>
<label>Brand name</label>
<p><?= $psu[PsuModel::VENDOR_NAME->value] ?></p>
</div>
<div>
<label>Brand model</label>
<p><?= $psu[PsuModel::VENDOR_MODEL->value] ?></p>
</div>
<div>
<label>Is modular?</label>
<p><?= $psu[PsuModel::TYPE_MODULAR->value] === "TRUE" ? "Yes" : "No" ?></p>
</div>
<div>
<label>80+ Rating</label>
<p><?= $psu[PsuModel::EIGHTYPLUS_RATING->value] ?? "None" ?></p>
</div>
<div>
<label>Aquired</label>
<p><?= date(API::DATE_FORMAT, $psu[PsuModel::DATE_AQUIRED->value]) ?></p>
</div>
<?php if ($psu[PsuModel::IS_RETIRED->value]): ?>
<div>
<label>Retired</label>
<p>Yes</p>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<div class="group">
<p>DRAM</p>
<?= VV::embed("assets/media/icons/chevron.svg") ?>
</div>
<div class="collection">
<?php // List all DRAM ?>
<?php foreach ($motherboard["dram"] as $mb_dram): ?>
<?php // Get case details from endpoint by id ?>
<?php $dram = $api->call(Endpoints::BATTLESTATION_DRAM->value)->params([
DramModel::ID->value => $mb_dram[MbDramModel::REF_DRAM_ID->value]
])->get()->json()[0]; ?>
<div data-target="dram" class="spec">
<p>DRAM - <?= $dram[DramModel::TECHNOLOGY->value] ?></p>
<h3><?= $dram[DramModel::VENDOR_NAME->value] ?>
<span><?= $dram[DramModel::CAPACITY->value] / GIGA ?>GB</span>
<span><?= $dram[DramModel::SPEED->value] / MEGA ?>MHz</span>
</h3>
<div>
<div>
<label>Capacity</label>
<p><?= $dram[DramModel::CAPACITY->value] / GIGA ?>GB</p>
</div>
<div>
<label>Speed</label>
<p><?= $dram[DramModel::SPEED->value] / MEGA ?>MHz</p>
</div>
<div>
<label>Brand name</label>
<p><?= $dram[DramModel::VENDOR_NAME->value] ?></p>
</div>
<div>
<label>Brand model</label>
<p><?= $dram[DramModel::VENDOR_MODEL->value] ?></p>
</div>
<div>
<label>Formfactor</label>
<p><?= $dram[DramModel::FORMFACTOR->value] ?></p>
</div>
<div>
<label>Technology</label>
<p><?= $dram[DramModel::TECHNOLOGY->value] ?></p>
</div>
<div>
<label>Is ECC?</label>
<p><?= $dram[DramModel::ECC->value] === "TRUE" ? "Yes" : "No" ?></p>
</div>
<div>
<label>Is buffered?</label>
<p><?= $dram[DramModel::BUFFERED->value] === "TRUE" ? "Yes" : "No" ?></p>
</div>
<div>
<label>Aquired</label>
<p><?= date(API::DATE_FORMAT, $dram[DramModel::DATE_AQUIRED->value]) ?></p>
</div>
<?php if ($dram[DramModel::IS_RETIRED->value]): ?>
<div>
<label>Retired</label>
<p>Yes</p>
</div>
<?php endif; ?>
</div>
<div>
<div>
<label>In motherboard slot number</label>
<p><?= $mb_dram[MbDramModel::SOCKET->value] ?></p>
</div>
<div>
<label>Motherboard slot type</label>
<p><?= $mb_dram[MbDramModel::SOCKET_TYPE->value] ?></p>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="group">
<p>Storage</p>
<?= VV::embed("assets/media/icons/chevron.svg") ?>
</div>
<div class="collection">
<?php // List all storage ?>
<?php foreach ($motherboard["storage"] as $mb_storage): ?>
<?php // Get case details from endpoint by id ?>
<?php $storage = $api->call(Endpoints::BATTLESTATION_STORAGE->value)->params([
StorageModel::ID->value => $mb_storage[MbStorageModel::REF_STORAGE_ID->value]
])->get()->json()[0]; ?>
<div data-target="drive" class="spec">
<p><?= $storage[StorageModel::DISK_FORMFACTOR->value] ?> <?= $storage[StorageModel::DISK_TYPE->value] ?></p>
<h3>
<?= $storage[StorageModel::VENDOR_NAME->value] ?>
<span><?= floor($storage[StorageModel::DISK_SIZE->value] / GIGA) ?>GB</span>
</h3>
<div>
<div>
<label>Type</label>
<p><?= $storage[StorageModel::DISK_TYPE->value] ?></p>
</div>
<div>
<label>Capacity</label>
<p><?= floor($storage[StorageModel::DISK_SIZE->value] / GIGA) ?>GB</p>
</div>
<div>
<label>Interface</label>
<p><?= $storage[StorageModel::DISK_INTERFACE->value] ?></p>
</div>
<div>
<label>Formfactor</label>
<p><?= $storage[StorageModel::DISK_FORMFACTOR->value] ?></p>
</div>
<div>
<label>Brand name</label>
<p><?= $storage[StorageModel::VENDOR_NAME->value] ?></p>
</div>
<div>
<label>Brand model</label>
<p><?= $storage[StorageModel::VENDOR_MODEL->value] ?></p>
</div>
<div>
<label>Aquired</label>
<p><?= date(API::DATE_FORMAT, $storage[StorageModel::DATE_AQUIRED->value]) ?></p>
</div>
<?php if ($storage[StorageModel::IS_RETIRED->value]): ?>
<div>
<label>Retired</label>
<p>Yes</p>
</div>
<?php endif; ?>
</div>
<div>
<div>
<label>Attatched via interface</label>
<p><?= $mb_storage[MbStorageModel::INTERFACE->value] ?></p>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
<?php endforeach; ?>
<?php endif; ?>
<script type="module"><?= VV::js("assets/js/pages/about/battlestation") ?></script>

0
assets/css/fonts.css → public/assets/css/fonts.css Executable file → Normal file
View file

View file

@ -0,0 +1,286 @@
/* # Overrides */
:root {
--primer-color-accent: 148, 255, 21;
--color-accent: rgb(var(--primer-color-accent));
--hue-accent: 390deg;
--primer-color-go: 0, 173, 216;
--primer-color-php: 79, 93, 149;
--primer-color-css: 86, 61, 124;
--primer-color-html: 227, 76, 38;
--primer-color-shell: 137, 224, 81;
--primer-color-python: 53, 114, 165;
--primer-color-typescript: 49, 120, 198;
--primer-color-javascript: 241, 224, 90;
--color-go: rgb(var(--primer-color-go));
--color-php: rgb(var(--primer-color-php));
--color-css: rgb(var(--primer-color-css));
--color-html: rgb(var(--primer-color-html));
--color-shell: rgb(var(--primer-color-shell));
--color-python: rgb(var(--primer-color-python));
--color-typescript: rgb(var(--primer-color-typescript));
--color-javascript: rgb(var(--primer-color-javascript));
}
vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
}
/* # Sections */
/* ## Divider */
vv-shell > hr {
border-color: rgba(255, 255, 255, .1);
}
/* ## About */
section.about {
display: flex;
flex-direction: column;
gap: calc(var(--padding) / 2);
}
section.about p:first-of-type:first-letter {
font-size: 1.8rem;
font-weight: bold;
margin-right: .1rem;
color: var(--color-accent);
}
section.about span.interests {
-webkit-user-select: none;
user-select: none;
color: var(--color-accent);
animation: interests-hue 5s infinite linear;
}
/* ## Languages */
section.languages {
margin: calc(var(--padding) / 1.5) 0;
}
section.languages stacked-bar-chart {
gap: 3px;
width: 100%;
display: flex;
border-radius: 100px;
height: var(--padding);
background-color: rgba(255, 255, 255, 0);
}
section.languages stacked-bar-chart:last-of-type {
flex-direction: row-reverse;
}
section.languages stacked-bar-chart:hover chart-segment {
opacity: .5;
}
section.languages stacked-bar-chart chart-segment {
--border-corner-radius: 100px;
transition: 150ms opacity;
width: var(--size, 0%);
min-width: 3%;
height: 100%;
color: white;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, .1);
border-radius: 2px;
}
section.languages stacked-bar-chart a:nth-child(odd) chart-segment {
background-color: rgba(255, 255, 255, .3);
}
/* ### Round corners */
section.languages stacked-bar-chart a:first-child chart-segment {
border-top-right-radius: var(--padding);
border-bottom-right-radius: var(--padding);
border-top-left-radius: var(--border-corner-radius);
border-bottom-left-radius: var(--border-corner-radius);
}
section.languages stacked-bar-chart a:last-child chart-segment {
border-top-left-radius: var(--padding);
border-bottom-left-radius: var(--padding);
border-top-right-radius: var(--border-corner-radius);
border-bottom-right-radius: var(--border-corner-radius);
}
section.languages stacked-bar-chart:last-of-type a:first-child chart-segment {
border-top-left-radius: var(--padding);
border-bottom-left-radius: var(--padding);
border-top-right-radius: var(--border-corner-radius);
border-bottom-right-radius: var(--border-corner-radius);
}
section.languages stacked-bar-chart:last-of-type a:last-child chart-segment {
border-top-right-radius: var(--padding);
border-bottom-right-radius: var(--padding);
border-top-left-radius: var(--border-corner-radius);
border-bottom-left-radius: var(--border-corner-radius);
}
/* ### Texts */
section.languages stacked-bar-chart chart-segment p {
text-align: center;
color: inherit;
overflow: hidden;
white-space: nowrap;
pointer-events: none;
text-overflow: ellipsis;
padding: 0 3px;
}
section.languages stacked-bar-chart chart-segment[style="--size:0%;"] p span {
display: none;
}
section.languages stacked-bar-chart chart-segment[style="--size:0%;"] p::before {
content: "<1%";
opacity: .5;
}
section.languages stacked-bar-chart chart-segment [data-hover] {
display: none;
}
/* ### Colors */
section.languages stacked-bar-chart a chart-segment[data-lang="Go"] { background-color: var(--color-go); }
section.languages stacked-bar-chart a chart-segment[data-lang="PHP"] { background-color: var(--color-php); }
section.languages stacked-bar-chart a chart-segment[data-lang="CSS"] { background-color: var(--color-css); }
section.languages stacked-bar-chart a chart-segment[data-lang="HTML"] { background-color: var(--color-html); }
section.languages stacked-bar-chart a chart-segment[data-lang="Python"] { background-color: var(--color-python); }
section.languages stacked-bar-chart a chart-segment[data-lang="TypeScript"] { background-color: var(--color-typescript); }
section.languages stacked-bar-chart a chart-segment[data-lang="Shell"] { background-color: var(--color-shell); color: black; }
section.languages stacked-bar-chart a chart-segment[data-lang="JavaScript"] { background-color: var(--color-javascript); color: black; }
/* ### Legend */
section.languages languages-list {
gap: calc(var(--padding) / 2);
display: grid;
grid-template-columns: repeat(3, 1fr);
margin: var(--padding) 0;
}
section.languages languages-list language-item {
gap: 10px;
display: flex;
border-radius: 8px;
align-items: center;
fill: var(--color-php);
padding: calc(var(--padding) / 1.5);
border: solid 1px rgba(255, 255, 255, .1);
background: linear-gradient(139deg, rgba(0, 0, 0, 0) 0%, rgba(79, 93, 144, .2) 100%);
}
section.languages languages-list language-item svg {
width: 2em;
margin-left: auto;
transform: rotate(-90deg);
}
section.languages button p.lang {
font-size: 1.3em;
font-weight: 900;
}
/* # Interests */
div.interests {
--text-shadow-blur: 30px;
transition: 300ms opacity;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
font-weight: bold;
pointer-events: none;
font-size: clamp(16px, 15vw, 50px);
color: var(--color-accent);
overflow: hidden;
opacity: 0;
z-index: 200;
}
div.interests.active {
opacity: 1;
}
div.interests p {
transition: 500ms transform cubic-bezier(.34,0,0,.99);
position: absolute;
text-shadow:
0 0 var(--text-shadow-blur) black,
0 0 var(--text-shadow-blur) black,
0 0 var(--text-shadow-blur) black,
0 0 var(--text-shadow-blur) black,
0 0 var(--text-shadow-blur) black;
}
@keyframes interests-hue {
to {
-webkit-filter: hue-rotate(360deg);
filter: hue-rotate(360deg);
}
}
/* Feature queries */
@media (hover: hover) {
section.languages stacked-bar-chart chart-segment:hover {
opacity: 1;
}
section.languages stacked-bar-chart chart-segment [data-hover] {
display: none;
position: absolute;
top: 0;
left: 0;
text-align: center;
transform: translate(0, 0);
background-color: inherit;
padding: 5px 10px;
white-space: nowrap;
pointer-events: none;
border-radius: 6px;
z-index: 2000;
-webkit-backdrop-filter: brightness(.2) blur(20px);
backdrop-filter: brightness(.2) blur(20px);
}
section.languages stacked-bar-chart chart-segment [data-hover].hovering {
display: initial;
}
}
/* Size queries */
@media (max-width: 900px) {
section.languages languages-list {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 650px) {
section.languages languages-list {
grid-template-columns: 1fr;
}
}

View file

@ -3,6 +3,7 @@
:root {
--primer-color-accent: 255, 195, 255;
--color-accent: rgb(var(--primer-color-accent));
--hue-accent: 200deg;
}
vv-shell {
@ -28,10 +29,6 @@ section.center {
text-align: center;
}
section.fade {
opacity: .3;
}
/* ## Social */
section.social {
@ -71,7 +68,7 @@ section.social social:hover {
fill: var(--color-accent);
}
section.social social.hovering p {
section.social social p.hovering {
display: initial;
}
@ -81,11 +78,12 @@ section.pgp {
max-width: 800px;
position: relative;
display: flex;
border-radius: 12px;
flex-direction: column;
gap: var(--padding);
text-align: center;
background-color: rgba(var(--primer-color-accent), .15);
padding: calc(var(--padding) * 1.5);
padding: var(--padding);
transform: rotate(-1.5deg);
}
@ -108,6 +106,19 @@ section.pgp .buttons {
gap: var(--padding);
}
/* ## Blockquote */
cite {
text-decoration: underline;
text-decoration-style: dotted;
text-decoration-color: rgba(var(--primer-color-accent), .5);
text-decoration-thickness: .1em;
&:hover {
text-decoration-color: var(--color-accent);
}
}
/* ## Contact form */
section.form :is(input, textarea) {
@ -196,6 +207,10 @@ section.form-message.sent + section.form {
/* # Size queries */
@media (min-width: 460px) {
section.pgp {
padding: calc(var(--padding) * 1.5);
}
section.pgp .buttons {
flex-direction: row;
justify-content: center;

View file

View file

@ -38,7 +38,7 @@ vv-shell img {
padding: unset;
text-align: right;
font-size: clamp(20px, 8vh, 60px);
font-weight: 900;
font-weight: bold;
line-height: clamp(20px, 8vh, 60px);
color: var(--color-accent);
}
@ -49,6 +49,11 @@ vv-shell img {
.menu svg {
width: 100%;
animation: dash 1500ms linear infinite;
}
@keyframes dash {
to { stroke-dashoffset: 32; }
}
/* ### Copy email button */

View file

@ -0,0 +1,84 @@
vv-shell[vv-page="/search"] {
display: flex;
flex-direction: column;
gap: calc(var(--padding) / 2);
}
/* # Search */
section.search {
display: flex;
gap: var(--padding);
border-radius: 6px;
padding: var(--padding);
background-color: rgba(255, 255, 255, .1);
}
section.search form {
display: contents;
}
section.search input {
flex: 1 1 auto;
border-radius: 6px;
padding: 0 var(--padding);
border: solid 2px rgba(255, 255, 255, .1);
background-color: rgba(255, 255, 255, .1);
}
section.search input:focus {
outline: none;
border-color: white;
}
section.search select {
padding: 5px;
border: none;
background-color: transparent;
}
section.search select :is(option, optgroup) {
color: black;
}
/* # Center */
section.center {
display: flex;
flex: 1 1 auto;
align-items: center;
flex-direction: column;
justify-content: center;
fill: var(--color-accent);
gap: calc(var(--padding) / 2);
}
section.center svg {
width: 60px;
}
/* # Result */
section.result {
display: flex;
}
section.result button {
flex: 1 1 auto;
text-align: left;
padding: var(--padding);
}
/* # Stats */
section.stats {
min-height: calc(var(--padding) * 2);
display: flex;
align-items: center;
gap: calc(var(--padding) / 2);
justify-content: space-between;
}
vv-shell[vv-page="/search"] section.stats button {
display: none;
}

View file

@ -0,0 +1,16 @@
/* # Overrides */
:root {
--primer-color-accent: 3, 255, 219;
--color-accent: rgb(var(--primer-color-accent));
--hue-accent: 90deg;
}
vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
width: 100%;
max-width: 1200px;
overflow-x: initial;
}

View file

@ -0,0 +1,167 @@
/* # Overrides */
:root {
--primer-color-accent: 3, 255, 219;
--color-accent: rgb(var(--primer-color-accent));
--hue-accent: 90deg;
--primer-color-reflect: 220, 26, 0;
--primer-color-vegvisir: 0, 128, 255;
}
vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
width: 100%;
max-width: 1200px;
overflow-x: initial;
}
/* # Sections */
/* ## Hero */
section.hero {
--color-accent: rgb(255, 255, 255);
display: grid;
gap: var(--padding);
grid-template-columns: repeat(1, 1fr);
}
section.hero .item {
width: 100%;
position: relative;
border-radius: 6px;
}
section.hero .wrapper {
gap: var(--padding);
z-index: 1;
height: 100%;
display: flex;
position: relative;
align-items: baseline;
flex-direction: column;
padding: calc(var(--padding) * 1.5);
}
section.hero .item .title {
display: grid;
align-items: center;
gap: var(--padding);
grid-template-columns: 40px 1fr;
}
section.hero .item .title svg {
height: 3em;
border-radius: 4px;
}
section.hero .actions {
margin-top: auto;
}
/* ### Vegivisr */
section.hero .item.vegvisir {
--primer-color-accent: var(--primer-color-vegvisir);
--color-accent: rgb(var(--primer-color-vegvisir));
color: var(--color-vegvisir);
background-color: rgba(var(--primer-color-vegvisir), .1);
}
/* ### Reflect */
section.hero .item.reflect {
--primer-color-accent: var(--primer-color-reflect);
--color-accent: rgb(var(--primer-color-reflect));
color: var(--color-reflect);
background-color: rgba(var(--primer-color-reflect), .2);
}
/* ## Heading */
section.heading {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
section.heading svg {
height: 2em;
}
/* ## Featured */
section.featured {
display: grid;
gap: var(--padding);
grid-template-columns: repeat(1, 1fr);
}
section.featured featured-item {
display: flex;
fill: white;
color: white;
border-radius: 8px;
align-items: baseline;
flex-direction: column;
padding: var(--padding);
background-color: rgba(255, 255, 255, .1);
}
section.featured featured-item .title {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: calc(var(--padding) / 2);
}
section.featured featured-item .title svg {
height: 2em;
fill: var(--color-accent);
}
section.featured featured-item img {
width: 100%;
border-radius: 4px;
}
section.featured featured-item .actions {
gap: var(--padding);
width: 100%;
display: flex;
flex-direction: column;
padding-top: var(--padding);
margin-top: auto;
}
/* # Size queries */
@media (min-width: 400px) {
section.featured featured-item .actions {
flex-direction: row;
}
}
@media (min-width: 600px) {
section.hero {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 900px) {
section.featured {
grid-template-columns: repeat(3, 1fr);
}
section.featured featured-item .actions button.collapse p {
display: none;
}
}

View file

@ -3,6 +3,7 @@
:root {
--primer-color-accent: 3, 255, 219;
--color-accent: rgb(var(--primer-color-accent));
--hue-accent: 90deg;
}
vv-shell {
@ -27,7 +28,7 @@ section.git {
border-radius: 6px;
}
section.git svg {
section.git > svg {
fill: white;
width: 60px;
}
@ -120,13 +121,10 @@ section.timeline .items .item img {
}
section.timeline .items .item .actions {
display: flex;
align-items: baseline;
margin-top: 7px;
}
/* ## Note */
section.note {
text-align: center;
gap: var(--padding);
}
/* # Size queries */
@ -137,23 +135,6 @@ section.note {
}
}
@media (min-width: 900px) {
section.git {
display: grid;
grid-template-columns: 70px 1fr 400px;
align-items: center;
gap: calc(var(--padding) * 1.5);
}
section.git svg {
width: 100%;
}
section.git .buttons {
justify-content: end;
}
}
@media (max-width: 500px) {
section.timeline {
padding: unset;
@ -188,7 +169,28 @@ section.note {
border-top-color: rgba(var(--primer-color-accent), .2);
}
section.timeline .items .item .actions {
flex-direction: column;
}
section.timeline .year:first-of-type .month:first-of-type .day:first-of-type .items .item:first-of-type {
margin-top: var(--padding);
}
}
@media (min-width: 900px) {
section.git {
display: grid;
grid-template-columns: 70px 1fr 400px;
align-items: center;
gap: calc(var(--padding) * 1.5);
}
section.git svg {
width: 100%;
}
section.git .buttons {
justify-content: end;
}
}

View file

@ -0,0 +1,16 @@
/* # Overrides */
:root {
--primer-color-accent: 3, 255, 219;
--color-accent: rgb(var(--primer-color-accent));
--hue-accent: 90deg;
}
vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
width: 100%;
max-width: 1200px;
overflow-x: initial;
}

View file

@ -11,7 +11,8 @@
/* # Cornerstones */
* {
margin: 0;
margin: 0;
fill: inherit;
box-sizing: border-box;
font-family: "Roboto Mono", sans-serif;
color: inherit;
@ -88,6 +89,32 @@ h3 {
font-size: 25px;
}
/* ## Page transition */
[vv-loading] * {
transition: 200ms opacity;
}
[vv-loading="true"] * {
opacity: 0;
pointer-events: none;
}
[vv-loading="true"]::after {
content: "";
position: fixed;
top: 50%;
left: 50%;
width: 45px;
height: 49px;
background-size: contain;
image-rendering: pixelated;
transform: translate(-50%, -50%);
background-image: url("/assets/media/spinner.gif");
-webkit-filter: hue-rotate(var(--hue-accent));
filter: hue-rotate(var(--hue-accent));
}
/* ## Buttons */
button {
@ -101,35 +128,44 @@ button {
/* ### Inline */
button.inline {
padding: calc(var(--padding) / 2) var(--padding);
color: white;
border: solid 2px white;
border-radius: 6px;
gap: 10px;
display: flex;
border-radius: 7px;
align-items: center;
fill: var(--color-accent);
padding: calc(var(--padding) / 1.5);
background: linear-gradient(139deg, rgba(0, 0, 0, 0) 0%, rgba(var(--primer-color-accent), .1) 100%);
}
button.inline:not(.solid) {
box-shadow:
0 0 0 2px rgba(var(--primer-color-accent), .1),
10px 7px 40px 3px rgba(var(--primer-color-accent), .06)
;
}
button.inline svg {
flex: none;
height: 1em;
}
button.inline svg:last-child {
width: 1.5em;
margin-left: auto;
}
button.inline svg.chevron:last-child {
transform: rotate(-90deg);
}
button.inline.solid {
fill: black;
color: black;
border: solid 2px rgba(var(--primer-color-accent), 1);
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 {
@ -250,6 +286,10 @@ header searchbox input {
border: none;
}
header searchbox input::placeholder {
color: rgba(0, 0, 0, .5);
}
/* #### Active */
header.searchboxActive > * {
@ -274,6 +314,9 @@ vv-shell {
search-results {
transition: 500ms opacity, 300ms transform;
position: fixed;
display: flex;
flex-direction: column;
gap: var(--padding);
top: var(--running-size);
right: 0;
width: 100%;
@ -285,12 +328,7 @@ search-results {
transform: scale(.99);
transform-origin: 100% 0;
overflow-y: scroll;
}
search-results:not([vv-page]) {
display: grid;
align-items: center;
justify-items: center;
z-index: 50;
}
header.searchboxActive ~ search-results {
@ -299,6 +337,10 @@ header.searchboxActive ~ search-results {
transform: scale(1);
}
search-results section.search {
display: none;
}
/* ### "Start typing" prompt */
search-results .info {
@ -306,11 +348,11 @@ search-results .info {
align-items: center;
flex-direction: column;
margin: auto;
gap: 3svh;
gap: var(--padding);
}
search-results .info :is(svg, img) {
width: 128px;
width: 60px;
fill: var(--color-accent);
}
@ -326,7 +368,8 @@ search-results .info :is(svg, img) {
/* # Components */
button.inline {
transition: 200ms background-color, 200ms border-color, 200ms color;
transition-duration: 300ms;
transition-property: background-color, border-color, box-shadow, color, fill;
}
button:hover {
@ -334,8 +377,19 @@ search-results .info :is(svg, img) {
background-color: rgba(255, 255, 255, .1);
}
button.solid:hover {
button.inline:hover {
fill: var(--color-accent);
color: var(--color-accent);
}
button.inline:not(.solid):hover {
box-shadow:
0 0 0 2px rgba(var(--primer-color-accent), 1),
10px 7px 30px 3px rgba(var(--primer-color-accent), .07)
;
}
button.solid:hover {
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);

View file

@ -0,0 +1,32 @@
import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
export const TARGET_SELECTOR = "[data-hover]";
export class Hoverpop {
/**
* Bind hover targets on provided Elevent-compatible element(s)
* @param {HTMLElement|HTMLElements|string} elements
*/
constructor(elements) {
// Bind hover targets on element(s)
new Elevent("mouseenter", elements, (event) => {
const element = event.target.querySelector(TARGET_SELECTOR);
// Bail out if target element contains no hover target element
if (!element) {
return;
}
element.classList.add("hovering");
event.target.addEventListener("mousemove", (event) => {
const x = event.layerX - (element.clientWidth / 2);
const y = event.layerY + element.clientHeight;
element.style.setProperty("transform", `translate(${x}px, ${y}px)`);
});
});
// Bind hover leave targets on element(s)
new Elevent("mouseleave", elements, () => elements.forEach(element => element.querySelector(TARGET_SELECTOR)?.classList.remove("hovering")));
}
}

View file

@ -56,12 +56,23 @@ const implodeInterests = () => {
// Bind mouse or touch events depending on pointer type of device
const canHover = window.matchMedia("(pointer: fine)").matches;
interestsElement.addEventListener(canHover ? "mouseenter" : "touchstart", () => {
// Get absolute position of the trigger element
const size = interestsElement.getBoundingClientRect();
explodeInterests(size.x, size.y);
});
// Explode interests when mouse hovers or touch hold starts
interestsElement.addEventListener(canHover ? "mouseenter" : "touchstart", (event) => explodeInterests(event.x, event.y));
// Implode interests when mouse leaves or touch hold ends
interestsElement.addEventListener(canHover ? "mouseleave" : "touchend", () => implodeInterests());
}
// Language bar chart hover tooltip
document.querySelectorAll("stacked-bar-chart chart-segment").forEach(element => {
const tooltipElement = element.querySelector("[data-hover]");
element.addEventListener("mouseenter", () => tooltipElement.classList.add("hovering"));
element.addEventListener("mouseleave", () => tooltipElement.classList.remove("hovering"));
element.addEventListener("mousemove", (event) => {
const x = event.layerX - (tooltipElement.clientWidth / 2);
const y = event.layerY + (tooltipElement.clientHeight - 30);
tooltipElement.style.setProperty("transform", `translate(${x}px, ${y}px)`);
});
});

View file

@ -60,27 +60,16 @@ class ContactForm {
form ? (new ContactForm(form)) : ContactForm.removeSavedMessage();
}
// Social links hover
{
const socialElementHover = (target) => {
const element = target.querySelector("p");
document.querySelectorAll("social").forEach(element => {
const tooltipElement = element.querySelector("[data-hover]");
target.classList.add("hovering");
target.addEventListener("mousemove", (event) => {
const x = event.layerX - (element.clientWidth / 2);
const y = event.layerY + element.clientHeight;
element.addEventListener("mouseenter", () => tooltipElement.classList.add("hovering"));
element.addEventListener("mouseleave", () => tooltipElement.classList.remove("hovering"));
element.style.setProperty("transform", `translate(${x}px, ${y}px)`);
});
};
element.addEventListener("mousemove", (event) => {
const x = event.layerX - (tooltipElement.clientWidth / 2);
const y = event.layerY + tooltipElement.clientHeight;
const elements = [...document.querySelectorAll("social")];
elements.forEach(element => {
element.addEventListener("mouseenter", () => socialElementHover(element));
element.addEventListener("mouseleave", () => {
elements.forEach(element => element.classList.remove("hovering"));
});
tooltipElement.style.setProperty("transform", `translate(${x}px, ${y}px)`);
});
}
});

View file

View file

@ -1,5 +1,3 @@
import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
// Click to copy email button
{
const EMAIL_CPY_ANIM_DUR_MSECONDS = 1000;
@ -55,7 +53,7 @@ import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
}, EMAIL_CPY_ANIM_DUR_MSECONDS + 100);
}
new Elevent("click", document.querySelector(".email"), async () => {
document.querySelector(".email").addEventListener("click", async () => {
try {
await navigator.clipboard.writeText("victor@vlw.se");
@ -103,6 +101,6 @@ import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
});
// Reset color on navigation
vv.Navigation.rootShellElement.addEventListener(vv.Navigation.EVENTS.STARTED, () => updateColor(), { once: true });
VV.shell.addEventListener(VV.EVENT.START, () => updateColor(), { once: true });
}
}

View file

View file

@ -0,0 +1,4 @@
// Redirect to work page if no href is defined
if (!new URLSearchParams(window.location.search).has("href")) {
new VV().navigate("/work");
}

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