From 140132fa72e65a559b52b3e47fd13c8e3afe8621 Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Mon, 1 Apr 2024 10:22:25 +0000 Subject: [PATCH] feat: release 1.0.0 (#1) * wip: 2024-02-13T12:59:17+0100 (1707825557) * wip: 2024-02-21T03:16:48+0100 (1708481808) * wip: 2024-02-21T20:50:20+0100 (1708545020) * wip: 2024-02-21T20:50:20+0100 (1708545020) * wip: 2024-03-01T13:17:58+0100 (1709295478) * wip: 2024-03-06T12:06:58+0100 (1709723218) * wip: 2024-03-07T15:07:57+0100 (1709820477) * wip: 2024-03-09T01:36:44+0100 (1709944604) * wip: 2024-03-14T23:24:12+0100 (1710455052) * wip: 2024-03-28T18:27:40+0100 (1711646860) * wip: 2024-03-28T18:27:40+0100 (1711646860) * feat: create README * wip: 2024-04-01T12:21:45+0200 (1711966905) --- .env.example.ini | 4 + .gitignore | 144 +-------- LICENSE | 0 README.md | 65 ++++ api/.env.example.ini | 11 + api/composer.json | 7 + api/composer.lock | 171 +++++++++++ api/endpoints/coffee/GET.php | 39 +++ api/endpoints/coffee/POST.php | 36 +++ api/endpoints/media/GET.php | 95 ++++++ api/endpoints/media/POST.php | 117 ++++++++ api/endpoints/media/srcset/GET.php | 106 +++++++ api/endpoints/media/srcset/POST.php | 55 ++++ api/endpoints/messages/POST.php | 79 +++++ api/endpoints/releases/POST.php | 223 ++++++++++++++ api/endpoints/search/GET.php | 128 ++++++++ api/endpoints/work/DELETE.php | 60 ++++ api/endpoints/work/GET.php | 150 ++++++++++ api/endpoints/work/PATCH.php | 201 +++++++++++++ api/endpoints/work/POST.php | 133 +++++++++ api/endpoints/work/actions/DELETE.php | 73 +++++ api/endpoints/work/actions/POST.php | 102 +++++++ api/endpoints/work/permalinks/GET.php | 60 ++++ api/endpoints/work/permalinks/POST.php | 83 ++++++ api/endpoints/work/tags/DELETE.php | 80 +++++ api/endpoints/work/tags/POST.php | 93 ++++++ api/src/databases/VLWdb.php | 50 ++++ api/src/databases/models/Coffee.php | 10 + api/src/databases/models/Media.php | 24 ++ api/src/databases/models/MediaSrcset.php | 10 + api/src/databases/models/Messages.php | 15 + api/src/databases/models/Work.php | 19 ++ api/src/databases/models/WorkActions.php | 14 + api/src/databases/models/WorkMedia.php | 10 + api/src/databases/models/WorkPermalinks.php | 11 + api/src/databases/models/WorkTags.php | 20 ++ assets/css/document.css | 311 ++++++++++++++++++++ assets/css/fonts.css | 16 + assets/css/pages/about.css | 105 +++++++ assets/css/pages/contact.css | 184 ++++++++++++ assets/css/pages/error.css | 51 ++++ assets/css/pages/index.css | 170 +++++++++++ assets/css/pages/search.css | 94 ++++++ assets/css/pages/work.css | 207 +++++++++++++ assets/fonts/roboto-mono-bold.woff2 | Bin 0 -> 40812 bytes assets/fonts/roboto-mono-regular.woff2 | Bin 0 -> 40704 bytes assets/js/document.js | 72 +++++ assets/js/modules/glitch/Generator.mjs | 79 +++++ assets/js/modules/glitch/Glitch.mjs | 41 +++ assets/js/modules/glitch/GlitchWorker.js | 54 ++++ assets/js/pages/about.js | 65 ++++ assets/js/pages/contact.js | 87 ++++++ assets/js/pages/error.js | 47 +++ assets/js/pages/index.js | 120 ++++++++ assets/js/pages/search.js | 25 ++ assets/js/pages/work.js | 1 + assets/media/gazing.jpg | Bin 0 -> 63085 bytes assets/media/glitch_b64/1.txt | 1 + assets/media/glitch_b64/2.txt | 1 + assets/media/glitch_b64/3.txt | 1 + assets/media/glitch_b64/4.txt | 1 + assets/media/icons/close.svg | 1 + assets/media/icons/email.svg | 1 + assets/media/icons/github.svg | 1 + assets/media/icons/libera.svg | 1 + assets/media/icons/mastodon.svg | 1 + assets/media/icons/pin.svg | 1 + assets/media/icons/search.svg | 1 + assets/media/line.svg | 1 + assets/media/vw.svg | 1 + composer.json | 5 + composer.lock | 56 ++++ pages/about.php | 73 +++++ pages/about/version.php | 19 ++ pages/contact.php | 81 +++++ pages/document.php | 71 +++++ pages/error.php | 6 + pages/index.php | 31 ++ pages/search.php | 107 +++++++ pages/work.php | 162 ++++++++++ 80 files changed, 4723 insertions(+), 128 deletions(-) create mode 100755 .env.example.ini mode change 100644 => 100755 .gitignore mode change 100644 => 100755 LICENSE create mode 100644 README.md create mode 100755 api/.env.example.ini create mode 100755 api/composer.json create mode 100755 api/composer.lock create mode 100755 api/endpoints/coffee/GET.php create mode 100755 api/endpoints/coffee/POST.php create mode 100755 api/endpoints/media/GET.php create mode 100755 api/endpoints/media/POST.php create mode 100755 api/endpoints/media/srcset/GET.php create mode 100755 api/endpoints/media/srcset/POST.php create mode 100755 api/endpoints/messages/POST.php create mode 100755 api/endpoints/releases/POST.php create mode 100755 api/endpoints/search/GET.php create mode 100755 api/endpoints/work/DELETE.php create mode 100755 api/endpoints/work/GET.php create mode 100755 api/endpoints/work/PATCH.php create mode 100755 api/endpoints/work/POST.php create mode 100755 api/endpoints/work/actions/DELETE.php create mode 100755 api/endpoints/work/actions/POST.php create mode 100755 api/endpoints/work/permalinks/GET.php create mode 100755 api/endpoints/work/permalinks/POST.php create mode 100755 api/endpoints/work/tags/DELETE.php create mode 100755 api/endpoints/work/tags/POST.php create mode 100755 api/src/databases/VLWdb.php create mode 100755 api/src/databases/models/Coffee.php create mode 100755 api/src/databases/models/Media.php create mode 100755 api/src/databases/models/MediaSrcset.php create mode 100755 api/src/databases/models/Messages.php create mode 100755 api/src/databases/models/Work.php create mode 100755 api/src/databases/models/WorkActions.php create mode 100755 api/src/databases/models/WorkMedia.php create mode 100755 api/src/databases/models/WorkPermalinks.php create mode 100755 api/src/databases/models/WorkTags.php create mode 100755 assets/css/document.css create mode 100755 assets/css/fonts.css create mode 100755 assets/css/pages/about.css create mode 100755 assets/css/pages/contact.css create mode 100755 assets/css/pages/error.css create mode 100755 assets/css/pages/index.css create mode 100755 assets/css/pages/search.css create mode 100755 assets/css/pages/work.css create mode 100755 assets/fonts/roboto-mono-bold.woff2 create mode 100755 assets/fonts/roboto-mono-regular.woff2 create mode 100755 assets/js/document.js create mode 100755 assets/js/modules/glitch/Generator.mjs create mode 100755 assets/js/modules/glitch/Glitch.mjs create mode 100755 assets/js/modules/glitch/GlitchWorker.js create mode 100755 assets/js/pages/about.js create mode 100755 assets/js/pages/contact.js create mode 100755 assets/js/pages/error.js create mode 100755 assets/js/pages/index.js create mode 100755 assets/js/pages/search.js create mode 100755 assets/js/pages/work.js create mode 100755 assets/media/gazing.jpg create mode 100755 assets/media/glitch_b64/1.txt create mode 100755 assets/media/glitch_b64/2.txt create mode 100755 assets/media/glitch_b64/3.txt create mode 100755 assets/media/glitch_b64/4.txt create mode 100755 assets/media/icons/close.svg create mode 100755 assets/media/icons/email.svg create mode 100755 assets/media/icons/github.svg create mode 100755 assets/media/icons/libera.svg create mode 100755 assets/media/icons/mastodon.svg create mode 100755 assets/media/icons/pin.svg create mode 100755 assets/media/icons/search.svg create mode 100755 assets/media/line.svg create mode 100755 assets/media/vw.svg create mode 100755 composer.json create mode 100755 composer.lock create mode 100755 pages/about.php create mode 100755 pages/about/version.php create mode 100755 pages/contact.php create mode 100755 pages/document.php create mode 100755 pages/error.php create mode 100755 pages/index.php create mode 100755 pages/search.php create mode 100755 pages/work.php diff --git a/.env.example.ini b/.env.example.ini new file mode 100755 index 0000000..12a228f --- /dev/null +++ b/.env.example.ini @@ -0,0 +1,4 @@ +[api] +base_url = "https://api.vlw.one/" +api_key = "" +verify_peer = 0 \ No newline at end of file diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index c6bba59..70d179d --- a/.gitignore +++ b/.gitignore @@ -1,130 +1,18 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* +assets/media/content -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +# Bootstrapping # +################# +vendor +.env.ini -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +Icon? +ehthumbs.db +Thumbs.db +.directory diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a518f2 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# vlw.se +This is the source code behind [vlw.se](https://vlw.se) which has been written from the ground up by me. This website is built on top of my [Vegvisir web framework](https://github.com/victorwesterlund/vegvisir) and my [Reflect API framework](https://github.com/victorwesterlund/reflect). + +# Installation +If you for whatever reason want to get this website up and running for yourself this is how that is done. + +This website is built for PHP 8.0+ and MariaDB 14+ (for the API database). + +## Website (Vegvisir) +1. **Download this repo** + + Git clone or download this repo to any local folder + ``` + git clone https://github.com/VictorWesterlund/vlw.se + ``` +2. **Download and install Vegvisir** + + Follow the installation instructions for [Vegvisir](https://github.com/victorwesterlund/vegvisir) and point the `site_path` variable to the local vlw.se folder. + +3. **Install dependencies** + + Install dependencies with composer. + ``` + composer install --optimize-autoloader + ``` + +Et voila! You probably want to install the API-side too but the website itself should now be accessible from your configured Vegvisir host. + +## 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://github.com/VictorWesterlund/vlw.se + ``` + +2. **Download and install Reflect** + + Follow the installation instructions for [Reflect](https://github.com/victorwesterlund/vegvisir) and point the `endpoints` variable to the `/api` subdirectory in the local vlw.se folder. + +3. **Install dependencies** + + 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. + + You also have to generate a [GitHub access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) if you wish to use the `releases` endpoint. + [Read more about this endpoint here](#) + +6. **Set environment variables for website** + + It's reasonable to assume if you've installed the website from this repo that you'd also want to use the API with it. Start my making a copy of `/.env.example.ini` (root directory) and change the `[api]` variables to point to your API hostname. diff --git a/api/.env.example.ini b/api/.env.example.ini new file mode 100755 index 0000000..b149618 --- /dev/null +++ b/api/.env.example.ini @@ -0,0 +1,11 @@ +[vlwdb] +mariadb_host = "" +mariadb_user = "" +mariadb_pass = "" +mariadb_db = "" + +[github] +api_key = "" +# Use-Agent string sent to GitHub API +# They recommend setting it to your GitHub username or app name +user_agent = "" \ No newline at end of file diff --git a/api/composer.json b/api/composer.json new file mode 100755 index 0000000..c85dbd6 --- /dev/null +++ b/api/composer.json @@ -0,0 +1,7 @@ +{ + "require": { + "reflect/plugin-rules": "^1.5", + "victorwesterlund/innodb-fk": "^1.0", + "victorwesterlund/xenum": "^1.1" + } +} diff --git a/api/composer.lock b/api/composer.lock new file mode 100755 index 0000000..46c636a --- /dev/null +++ b/api/composer.lock @@ -0,0 +1,171 @@ +{ + "_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": "ba3fa8466aa20501e06050d722c86a35", + "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/innodb-fk", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/VictorWesterlund/php-libinnodb-fk.git", + "reference": "ffea024f16613e6d6857c93200185cf0a20a9640" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/VictorWesterlund/php-libinnodb-fk/zipball/ffea024f16613e6d6857c93200185cf0a20a9640", + "reference": "ffea024f16613e6d6857c93200185cf0a20a9640", + "shasum": "" + }, + "require": { + "victorwesterlund/libmysqldriver": "^3.0", + "victorwesterlund/xenum": "^1.0" + }, + "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": "Retrievie and optionally resolves foreign keys in a MySQL/MariaDB InnoDB database", + "support": { + "issues": "https://github.com/VictorWesterlund/php-libinnodb-fk/issues", + "source": "https://github.com/VictorWesterlund/php-libinnodb-fk/tree/1.0.3" + }, + "time": "2023-11-02T13:26:34+00:00" + }, + { + "name": "victorwesterlund/libmysqldriver", + "version": "3.5.1", + "source": { + "type": "git", + "url": "https://github.com/VictorWesterlund/php-libmysqldriver.git", + "reference": "73b5d858ffa8d83c5cbe6b3d3de4af314a5ffbe5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/VictorWesterlund/php-libmysqldriver/zipball/73b5d858ffa8d83c5cbe6b3d3de4af314a5ffbe5", + "reference": "73b5d858ffa8d83c5cbe6b3d3de4af314a5ffbe5", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "libmysqldriver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-only" + ], + "authors": [ + { + "name": "Victor Westerlund", + "email": "victor.vesterlund@gmail.com" + } + ], + "description": "Abstraction library for common mysqli features", + "support": { + "issues": "https://github.com/VictorWesterlund/php-libmysqldriver/issues", + "source": "https://github.com/VictorWesterlund/php-libmysqldriver/tree/3.5.1" + }, + "time": "2024-02-26T12:51:52+00:00" + }, + { + "name": "victorwesterlund/xenum", + "version": "1.1.1", + "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": "" + }, + "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" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/api/endpoints/coffee/GET.php b/api/endpoints/coffee/GET.php new file mode 100755 index 0000000..4ee9144 --- /dev/null +++ b/api/endpoints/coffee/GET.php @@ -0,0 +1,39 @@ +db->for(CoffeeModel::TABLE) + ->order([CoffeeModel::DATE_TIMESTAMP_CREATED->value => "DESC"]) + ->limit(self::LIST_LIMIT) + ->select([ + CoffeeModel::ID->value, + CoffeeModel::DATE_TIMESTAMP_CREATED->value + ]); + + return parent::is_mysqli_result($resp) + ? new Response($resp->fetch_all(MYSQLI_ASSOC)) + : $this->resp_database_error(); + } + } \ No newline at end of file diff --git a/api/endpoints/coffee/POST.php b/api/endpoints/coffee/POST.php new file mode 100755 index 0000000..5643942 --- /dev/null +++ b/api/endpoints/coffee/POST.php @@ -0,0 +1,36 @@ +db->for(CoffeeModel::TABLE) + ->insert([ + CoffeeModel::ID->value => $id, + CoffeeModel::DATE_TIMESTAMP_CREATED->value => time(), + ]); + + // Return 201 Created and entity id if successful + return $insert ? new Response($id, 201) : $this->resp_database_error(); + } + } \ No newline at end of file diff --git a/api/endpoints/media/GET.php b/api/endpoints/media/GET.php new file mode 100755 index 0000000..bda4957 --- /dev/null +++ b/api/endpoints/media/GET.php @@ -0,0 +1,95 @@ +ruleset = new Ruleset(strict: true); + + $this->ruleset->GET([ + (new Rules(MediaModel::ID->value)) + ->required() + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), + + (new Rules(self::GET_DISPOSITION_KEY)) + ->type(Type::ENUM, MediaDispositionEnum::values()) + ->default(MediaDispositionEnum::METADATA->value) + ]); + } + + // # Helper methods + + private function fetch_srcset(string $id): array { + $resp = $this->db->for(WorkTagsModel::TABLE) + ->where([WorkTagsModel::ANCHOR->value => $id]) + ->select(WorkTagsModel::NAME->value); + + return parent::is_mysqli_result($resp) ? $resp->fetch_all(MYSQLI_ASSOC) : []; + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to get work data, please try again later", 503); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + $resp = $this->db->for(MediaModel::TABLE) + ->where([MediaModel::ID->value => $_GET[MediaModel::ID->value]]) + ->select([ + MediaModel::ID->value, + MediaModel::NAME->value, + MediaModel::TYPE->value, + MediaModel::MIME->value, + MediaModel::EXTENSION->value, + MediaModel::SRCSET->value, + MediaModel::DATE_TIMESTAMP_CREATED->value, + ]); + + // Bail out if something went wrong retrieving rows from the database + if (!parent::is_mysqli_result($resp)) { + return $this->resp_database_error(); + } + + $media = $resp->fetch_assoc(); + $test = true; + } + } \ No newline at end of file diff --git a/api/endpoints/media/POST.php b/api/endpoints/media/POST.php new file mode 100755 index 0000000..1bd7916 --- /dev/null +++ b/api/endpoints/media/POST.php @@ -0,0 +1,117 @@ +ruleset = new Ruleset(strict: true); + + $this->ruleset->POST([ + (new Rules(MediaModel::ID->value)) + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ->default(parent::gen_uuid4()), + + (new Rules(MediaModel::NAME->value)) + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ->default(null), + + (new Rules(MediaModel::TYPE->value)) + ->type(Type::ENUM, MediaTypeEnum::values()) + ->default(null), + + (new Rules(MediaModel::EXTENSION->value)) + ->type(Type::STRING) + ->min(3) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ->default(null), + + (new Rules(MediaModel::MIME->value)) + ->type(Type::STRING) + ->min(3) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ->default(null), + + (new Rules(MediaModel::SRCSET->value)) + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ->default(null) + ]); + } + + // # Helper methods + + // Returns true if an srcset exists for provided key + private static function media_srcset_exists(): bool { + // No srcet get parameter has been set + if (empty($_POST[MediaModel::SRCSET->value])) { + return true; + } + + // Check if the provided srcset exists by calling the srcset endpoint + return Call("media/srcset?id={$_POST[MediaModel::SRCSET->value]}", Method::GET)->ok; + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to get work data, please try again later", 503); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + // Bail out if an srcset doesn't exist + if (!self::media_srcset_exists()) { + return new Response("No media srcset exists with id '{$_POST[MediaModel::SRCSET->value]}'", 404); + } + + $insert = $this->db->for(MediaModel::TABLE) + ->insert([ + MediaModel::ID->value => $_POST[MediaModel::ID->value], + MediaModel::NAME->value => $_POST[MediaModel::NAME->value], + MediaModel::MIME->value => $_POST[MediaModel::MIME->value], + // Strip dots from extension string if set + MediaModel::EXTENSION->value => $_POST[MediaModel::EXTENSION->value] + ? str_replace(".", "", $_POST[MediaModel::EXTENSION->value]) + : null, + MediaModel::SRCSET->value => $_POST[MediaModel::SRCSET->value], + MediaModel::DATE_TIMESTAMP_CREATED->value => time() + ]); + + // Return media id if insert was successful + return $insert + ? new Response($_POST[MediaModel::ID->value], 201) + : $this->resp_database_error(); + } + } \ No newline at end of file diff --git a/api/endpoints/media/srcset/GET.php b/api/endpoints/media/srcset/GET.php new file mode 100755 index 0000000..f9a0599 --- /dev/null +++ b/api/endpoints/media/srcset/GET.php @@ -0,0 +1,106 @@ +ruleset = new Ruleset(strict: true); + + $this->ruleset->GET([ + (new Rules(MediaSrcsetModel::ID->value)) + ->required() + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ]); + } + + // # Helper methods + + // Get metadata for the requested srcset + private function get_srcset(): array|false { + $srcset = $this->db->for(MediaSrcsetModel::TABLE) + ->where([MediaSrcsetModel::ID->value => $_GET[MediaSrcsetModel::ID->value]]) + ->select([MediaSrcsetModel::ANCHOR_DEFAULT->value]); + + // Something went wrong retrieving rows from the database + if (!parent::is_mysqli_result($srcset)) { + return false; + } + + // Return assoc array of srcset data if it exists + return $srcset->num_rows === 1 ? $srcset->fetch_assoc() : false; + } + + // Get all media entities that are part of the requested srcset + private function get_srcset_media(): mysqli_result|false { + $media = $this->db->for(MediaModel::TABLE) + ->where([MediaModel::SRCSET->value => $_GET[MediaSrcsetModel::ID->value]]) + ->select([ + MediaModel::ID->value, + MediaModel::TYPE->value, + MediaModel::MIME->value, + MediaModel::EXTENSION->value + ]); + + return parent::is_mysqli_result($media) ? $media : false; + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to get work data, please try again later", 503); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + // Get srcset data + $srcset = $this->get_srcset(); + if (!$srcset) { + return new Response("No media srcset exist with id '{$_GET[MediaSrcsetModel::ID->value]}'", 404); + } + + $media = $this->get_srcset_media(); + if (!$media) { + return new Response("Failed to fetch srcset media", 500); + } + + $media_entities = $media->fetch_all(MYSQLI_ASSOC); + + // This is the id of the media entity that is considered the default or "fallback" + $srcet_default_media_id = $srcset[MediaSrcsetModel::ANCHOR_DEFAULT->value]; + + // Return assoc array of all media entities that are in this srcset + return new Response([ + // Return default media entity separately from the rest of the srcset as an assoc array + "default" => array_filter($media_entities, fn(array $entity) => $entity[MediaModel::ID->value] === $srcet_default_media_id)[0], + // Return all media that isn't default as array of assoc arrays + "srcset" => array_filter($media_entities, fn(array $entity) => $entity[MediaModel::ID->value] !== $srcet_default_media_id) + ]); + } + } \ No newline at end of file diff --git a/api/endpoints/media/srcset/POST.php b/api/endpoints/media/srcset/POST.php new file mode 100755 index 0000000..609624a --- /dev/null +++ b/api/endpoints/media/srcset/POST.php @@ -0,0 +1,55 @@ +code !== 404) { + // Wow a UUID4 collision... buy a lottery ticket + if ($srcset_existing->code === 200) { + return $this->main(); + } + + // Failed to get srcset + return new Response("Something went wrong when checking if the srcset exists", 500); + } + + // Create new srcset entity + $insert = $this->db->for(MediaSrcsetModel::TABLE) + ->insert([ + MediaSrcsetModel::ID->value => $id + ]); + + // Return created srcset id if successful + return $insert + ? new Response($id, 201) + : $this->resp_database_error(); + } + } \ No newline at end of file diff --git a/api/endpoints/messages/POST.php b/api/endpoints/messages/POST.php new file mode 100755 index 0000000..e2a7758 --- /dev/null +++ b/api/endpoints/messages/POST.php @@ -0,0 +1,79 @@ +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) + ]); + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to get work data, please try again later", 503); + } + + public function main(): Response { + //return new Response(["hello" => "maybe"], 500); + + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + // Generate UUID for entity + $id = parent::gen_uuid4(); + + // Attempt to create new entity + $insert = $this->db->for(MessagesModel::TABLE) + ->insert([ + MessagesModel::ID->value => $id, + MessagesModel::EMAIL->value => $_POST["email"], + MessagesModel::MESSAGE->value => $_POST["message"], + MessagesModel::DATE_TIMESTAMP_CREATED->value => time(), + ]); + + // Bail out if insert failed + if (!$insert) { + return $this->resp_database_error(); + } + + // Return 201 Created and entity id + return new Response($id, 201); + } + } \ No newline at end of file diff --git a/api/endpoints/releases/POST.php b/api/endpoints/releases/POST.php new file mode 100755 index 0000000..7160ada --- /dev/null +++ b/api/endpoints/releases/POST.php @@ -0,0 +1,223 @@ +ruleset = new Ruleset(strict: true); + + $this->ruleset->POST([ + (new Rules(ReleasesPostModel::GITHUB_USER->value)) + ->required() + ->type(Type::STRING) + ->min(1), + + (new Rules(ReleasesPostModel::GITHUB_REPO->value)) + ->required() + ->type(Type::STRING) + ->min(1), + + (new Rules(ReleasesPostModel::GITHUB_TAG->value)) + ->required() + ->type(Type::STRING) + ->type(Type::NUMBER) + ->min(1) + ]); + + $this->curl = curl_init(); + + curl_setopt($this->curl, CURLOPT_USERAGENT, $_ENV["github"]["user_agent"]); + curl_setopt($this->curl, CURLOPT_HEADER, true); + curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($this->curl, CURLOPT_HTTPHEADER, [ + "Accept" => "application/vnd.github+json", + "Authorization" => "token {$_ENV["github"]["api_key"]}", + "X-GitHub-Api-Version" => "2022-11-28" + ]); + } + + // # GitHub + + // Generate HTML from a GitHub "auto-generate" release body + protected static function gh_auto_release_md_to_html(string $md): string { + $output = ""; + + // Parse each line of markdown + $lines = explode(PHP_EOL, $md); + + foreach ($lines as $i => $line) { + // Ignore header line from releases + if ($i < 1) continue; + + // Replace all URLs with HTMLAnchor tags, they will be PRs + $links = []; + preg_match_all(self::REGEX_URL, $line, $links, PREG_UNMATCHED_AS_NULL); + foreach ($links as $i => $link) { + if (empty($link)) continue; + + // Last crumb from link pathname will be the PR id + $pr_id = explode("/", $link[$i]); + $pr_id = end($pr_id); + + $line = str_replace($link, "{$pr_id}", $line); + } + + // Replace all at-handles with links to GitHub user profiles + $handles = []; + preg_match_all(self::REGEX_HANDLE, $line, $handles, PREG_UNMATCHED_AS_NULL); + foreach ($handles as $i => $handle) { + if (empty($handle)) continue; + + // GitHub user URL without the "@" + $url = "https://github.com/" . substr($handle[$i], 1); + + $line = str_replace($handle, "{$handle[$i]}", $line); + } + + $output .= "

{$line}

"; + } + + return $output; + } + + // Return fully qualified URL to GitHub API releases endpoint + private static function get_url(): string { + return implode("/", [ + self::GITHUB_API, + "repos", + $_POST[ReleasesPostModel::GITHUB_USER->value], + $_POST[ReleasesPostModel::GITHUB_REPO->value], + "releases", + "tags", + $_POST[ReleasesPostModel::GITHUB_TAG->value], + ]); + } + + // Fetch release information from GitHub API + private function fetch_release_data(): array { + $url = self::get_url(); + curl_setopt($this->curl, CURLOPT_URL, self::get_url()); + + $resp = curl_exec($this->curl); + + $header_size = curl_getinfo($this->curl, CURLINFO_HEADER_SIZE); + $header = substr($resp, 0, $header_size); + $body = substr($resp, $header_size); + + return json_decode($body, true); + } + + // # Sup + + private function create_link_to_release_page(string $id, string $href): Response { + return Call("work/actions?id={$id}", Method::POST, [ + WorkActionsModel::DISPLAY_TEXT->value => "Release details", + WorkActionsModel::HREF->value => $href, + WorkActionsModel::EXTERNAL->value => true + ]); + } + + // Create a tag for entity + private function create_tag(string $id, WorkTagsNameEnum $tag): Response { + return Call("work/tags?id={$id}", Method::POST, [ + // Set "RELEASE" tag on new entity + WorkTagsModel::NAME->value => $tag->value + ]); + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + $data = $this->fetch_release_data(); + if (!$data) { + return new Response("Failed to fetch release data", 500); + } + + + // Transform repo name to lowercase for summary title + $title = strtolower($_POST["repo"]); + + // Use repo name and tag name as heading for summary + $summary = "

Release {$title}@{$data["name"]}

"; + // Append HTML-ified release notes from GitHub to summary + $summary .= self::gh_auto_release_md_to_html($data["body"]); + + $date_published = new \DateTime($data["published_at"], new \DateTimeZone("UTC")); + + // Create work entity + $work_entity = Call("work", Method::POST, [ + WorkModel::SUMMARY->value => $summary, + // Convert time created to Unix timestamp for work endpoint + WorkModel::DATE_TIMESTAMP_CREATED->value => $date_published->format("U"), + ]); + + // Bail out if creating the work entity failed + if (!$work_entity->ok) { + return new Response("Failed to create work entity for release", 500); + } + + $work_entity_id = $work_entity->output(); + + // Create entity tags for release + $tags = [ + WorkTagsNameEnum::VLW, + WorkTagsNameEnum::RELEASE + ]; + foreach ($tags as $tag) { + // Create entity tag for release or exit if failed to create + if (!$this->create_tag($work_entity_id, $tag)->ok) { + return new Response("Failed to create {$tag->name} tag for release entity", 500); + } + } + + // Create link to release page on GitHub + if (!$this->create_link_to_release_page($work_entity_id, $data["html_url"])) { + return new Response("Failed to create link to release page on GitHub", 500); + } + + return new Response($work_entity_id, 201); + } + } \ No newline at end of file diff --git a/api/endpoints/search/GET.php b/api/endpoints/search/GET.php new file mode 100755 index 0000000..564f8b0 --- /dev/null +++ b/api/endpoints/search/GET.php @@ -0,0 +1,128 @@ +ruleset = new Ruleset(strict: true); + + $this->ruleset->GET([ + (new Rules(self::GET_QUERY)) + ->required() + ->type(Type::STRING) + ->min(2) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ]); + } + + // Return an SQL string from array for use in prepared statements + private static function array_to_wildcard_sql(array $columns): string { + $sql = array_map(fn(string $column): string => "{$column} LIKE CONCAT('%', ?, '%')", $columns); + + return implode(" OR ", $sql); + } + + // Return chained AND statements from array for use in prepared statements + private static function array_to_and_statement(array $keys): string { + $sql = array_map(fn(string $k): string => "{$k} = ?", $keys); + + return implode(" AND ", $sql); + } + + // Wildcard search columns in table with query string from query string + // This has to be implemented manually until "libmysqldriver/MySQL" supports wildcard SELECT + private function search(string $table, array $columns, array $conditions = null): array { + // Create CSV from columns array + $columns_concat = implode(",", $columns); + + // Create SQL LIKE wildcard statement for each column. + $where = self::array_to_wildcard_sql($columns); + + // Create array of values from query string for each colum + $values = array_fill(0, count($columns), $_GET[self::GET_QUERY]); + + if ($conditions) { + $conditions_sql = self::array_to_and_statement(array_keys($conditions)); + + // Wrap positive where statements and prepare new group of conditions + // WHERE () AND () + $where = "({$where}) AND ({$conditions_sql})"; + + // Append values from conditions statements to prepared statement + array_push($values, ...array_values($conditions)); + } + + // Order the rows by the array index of $colums received + $rows = $this->db->exec("SELECT {$columns_concat} FROM {$table} WHERE {$where} ORDER BY {$columns_concat}", $values); + // Return results as assoc or empty array + return parent::is_mysqli_result($rows) ? $rows->fetch_all(MYSQLI_ASSOC) : []; + } + + // Search work table + private function search_work(): array { + $search = [ + WorkModel::TITLE->value, + WorkModel::SUMMARY->value, + WorkModel::DATE_TIMESTAMP_CREATED->value, + WorkModel::ID->value + ]; + + $conditions = [ + WorkModel::IS_LISTABLE->value => true + ]; + + return $this->search(WorkModel::TABLE, $search, $conditions); + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to get work data, please try again later", 503); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + // Get search results for each category + $categories = [ + WorkModel::TABLE => $this->search_work() + ]; + + // Count total number of results from all categories + $total_num_results = 0; + foreach (array_values($categories) as $results) { + $total_num_results += count($results); + } + + return new Response([ + "query" => $_GET[self::GET_QUERY], + "results" => $categories, + "total_num_results" => $total_num_results + ]); + } + } \ No newline at end of file diff --git a/api/endpoints/work/DELETE.php b/api/endpoints/work/DELETE.php new file mode 100755 index 0000000..d197646 --- /dev/null +++ b/api/endpoints/work/DELETE.php @@ -0,0 +1,60 @@ +ruleset = new Ruleset(strict: true); + + $this->ruleset->GET([ + (new Rules("id")) + ->required() + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ]); + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to delete work data, please try again later", 503); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + // Attempt to update the entity + $update = $this->db->for(WorkModel::TABLE) + ->where([WorkModel::ID->value => $_GET["id"]]) + ->update([ + WorkModel::IS_LISTABLE->value => false, + WorkModel::IS_READABLE->value => false + ]); + + return $update ? new Response($_GET["id"]) : $this->resp_database_error(); + } + } \ No newline at end of file diff --git a/api/endpoints/work/GET.php b/api/endpoints/work/GET.php new file mode 100755 index 0000000..a8ef1d1 --- /dev/null +++ b/api/endpoints/work/GET.php @@ -0,0 +1,150 @@ +ruleset = new Ruleset(strict: true); + + $this->ruleset->GET([ + (new Rules("id")) + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ->default(null) + ]); + } + + // # Helper methods + + private function fetch_row_tags(string $id): array { + $resp = $this->db->for(WorkTagsModel::TABLE) + ->where([WorkTagsModel::ANCHOR->value => $id]) + ->select(WorkTagsModel::NAME->value); + + return parent::is_mysqli_result($resp) ? $resp->fetch_all(MYSQLI_ASSOC) : []; + } + + private function fetch_row_actions(string $id): array { + $resp = $this->db->for(WorkActionsModel::TABLE) + ->where([WorkActionsModel::ANCHOR->value => $id]) + ->select([ + WorkActionsModel::DISPLAY_TEXT->value, + WorkActionsModel::HREF->value, + WorkActionsModel::CLASS_LIST->value, + WorkActionsModel::EXTERNAL->value + ]); + + return parent::is_mysqli_result($resp) ? $resp->fetch_all(MYSQLI_ASSOC) : []; + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to get work data, please try again later", 503); + } + + private function resp_item_details(string $id): Response { + $resp = $this->db->for(WorkModel::TABLE) + ->where([ + WorkModel::ID->value => $id, + WorkModel::IS_READABLE->value => true + ]) + ->limit(1) + ->select([ + WorkModel::ID->value, + WorkModel::TITLE->value, + WorkModel::SUMMARY->value, + WorkModel::COVER_SRCSET->value, + WorkModel::DATE_YEAR->value, + WorkModel::DATE_MONTH->value, + WorkModel::DATE_DAY->value, + WorkModel::DATE_TIMESTAMP_MODIFIED->value, + WorkModel::DATE_TIMESTAMP_CREATED->value + ]); + + // Bail out if something went wrong retrieving rows from the database + if (!parent::is_mysqli_result($resp)) { + return $this->resp_database_error(); + } + + return $resp->num_rows === 1 + ? new Response($resp->fetch_assoc()) + : new Response("No entity with id '{$id}' was found", 404); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + // Return details about a specific item by id + if (!empty($_GET["id"])) { + return $this->resp_item_details($_GET["id"]); + } + + $resp = $this->db->for(WorkModel::TABLE) + ->where([WorkModel::IS_LISTABLE->value => true]) + ->order([WorkModel::DATE_TIMESTAMP_CREATED->value => "DESC"]) + ->select([ + WorkModel::ID->value, + WorkModel::TITLE->value, + WorkModel::SUMMARY->value, + WorkModel::COVER_SRCSET->value, + WorkModel::DATE_YEAR->value, + WorkModel::DATE_MONTH->value, + WorkModel::DATE_DAY->value, + WorkModel::DATE_TIMESTAMP_MODIFIED->value, + WorkModel::DATE_TIMESTAMP_CREATED->value + ]); + + // Bail out if something went wrong retrieving rows from the database + if (!parent::is_mysqli_result($resp)) { + return $this->resp_database_error(); + } + + // Resolve foreign keys + $rows = []; + while ($row = $resp->fetch_assoc()) { + $row["tags"] = $this->fetch_row_tags($row["id"]); + $row["actions"] = $this->fetch_row_actions($row["id"]); + + // Resolve media entities in srcset + $srcset = Call("media/srcset?id={$row[WorkModel::COVER_SRCSET->value]}", Method::GET); + + // Mutate key on current row + $row[WorkModel::COVER_SRCSET->value] = $srcset->ok ? $srcset->output() : []; + + $rows[] = $row; + } + + return new Response($rows); + } + } \ No newline at end of file diff --git a/api/endpoints/work/PATCH.php b/api/endpoints/work/PATCH.php new file mode 100755 index 0000000..e8e9be1 --- /dev/null +++ b/api/endpoints/work/PATCH.php @@ -0,0 +1,201 @@ +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_TIMESTAMP_CREATED->value)) + ->type(Type::NUMBER) + ->min(0) + ->max(parent::MYSQL_INT_MAX_LENGHT) + ]); + + $this->get_existing_entity(); + + // Copy all provided post data into a new array + $this->updated_entity = $_POST; + + // Set date modified timestamp + $this->updated_entity[WorkModel::DATE_TIMESTAMP_MODIFIED->value] = time(); + } + + // Generate a slug URL from string + private static function gen_slug(string $input): string { + return strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $input))); + } + + // # Helper methods + + private function get_existing_entity(): Response { + // Check if an entity already exists with slugified title from GET endpoint + $this->current_entity = Call("work?id={$_GET["id"]}", Method::GET); + + // Response is not 404 (Not found) so we can't create the entity + if ($this->current_entity->code !== 200) { + // Response is not a valid entity, something went wrong + if ($this->current_entity->code !== 404) { + return $this->resp_database_error(); + } + + // Return 402 Conflict + return new Response("No entity with id '{$_GET["id"]}' was found", 404); + } + + return $this->current_entity; + } + + // Create new permalink for entity slug + private function create_permalink(string $slug): bool { + $create = Call("work/permalinks", Method::POST, [ + WorkPermalinksModel::SLUG->value => $slug, + WorkPermalinksModel::ANCHOR->value => $slug + ]); + + return $create->ok; + } + + // ## Updated entity + + private function change_slug(): bool { + if (!array_key_exists(WorkModel::ID->value, $this->updated_entity)) { + return true; + } + + // Generate new permalink for entity id + return $this->create_permalink($this->updated_entity[WorkModel::ID->value]); + } + + private function timestamp_to_dates(): void { + if (!array_key_exists(WorkModel::DATE_TIMESTAMP_CREATED->value, $this->updated_entity)) { + return; + } + + // Get timestamp from post data + $timestamp = $this->updated_entity[WorkModel::DATE_TIMESTAMP_CREATED->value]; + + // Update fractured dates from timestamp + $this->updated_entity[WorkModel::DATE_YEAR->value] = date("Y", $timestamp); + $this->updated_entity[WorkModel::DATE_MONTH ->value] = date("n", $timestamp); + $this->updated_entity[WorkModel::DATE_DAY->value] = date("j", $timestamp); + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to get work data, please try again later", 503); + } + + // Return a 422 Unprocessable Entity if there is nothing to change + private function resp_no_changes(): Response { + return new Response("No columns to update", 422); + } + + // Rollback changes and return error response + private function resp_permalink_error_rollback(): Response { + $update = $this->db->for(WorkModel::TABLE) + ->where([WorkModel::ID->value => $_GET["id"]]) + ->update($this->current_entity->output()); + + return $update + ? new Response("Failed to create new permalink for updated entity. Changes have been rolled back", 500) + : new Reponse("Failed to create new permalink for updated entity. Changes failed to rollback, this is bad.", 500); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + // Empty payload, nothing to do + if (empty($_POST)) { + return $this->resp_no_changes(); + } + + // Generate new slug for entity if title is updated + if (array_key_exists(WorkModel::TITLE->value, $_POST)) { + // Generate URL slug from title text or UUID if undefined + $slug = self::gen_slug($_POST["title"]); + + // Save generated slug from title if it's different from existing slug + if ($slug !== $this->current_entity->output()[WorkModel::ID->value]) { + $this->updated_entity[WorkModel::ID->value] = $slug; + } + } + + // Update fractured dates from timestamp + $this->timestamp_to_dates(); + + // Attempt to update the entity + $update = $this->db->for(WorkModel::TABLE) + ->where([WorkModel::ID->value => $_GET["id"]]) + ->update($this->updated_entity); + + // Bail out if update failed + if (!$update) { + return $this->resp_database_error(); + } + + // Create new slug for entity if title was changed + if (!$this->change_slug()) { + return $this->resp_permalink_error_rollback(); + } + + // Return 200 OK and new or existing entity slug as body + return new Response($this->current_entity->output()[WorkModel::ID->value]); + } + } \ No newline at end of file diff --git a/api/endpoints/work/POST.php b/api/endpoints/work/POST.php new file mode 100755 index 0000000..90d0f0c --- /dev/null +++ b/api/endpoints/work/POST.php @@ -0,0 +1,133 @@ +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::DATE_TIMESTAMP_CREATED->value)) + ->type(Type::NUMBER) + ->min(1) + ->max(parent::MYSQL_INT_MAX_LENGHT) + ->default(null) + ]); + } + + // Generate a slug URL from string + private static function gen_slug(string $input): string { + return strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $input))); + } + + // Create permalink for entity slug + private function create_permalink(string $slug): bool { + $create = Call("work/permalinks", Method::POST, [ + WorkPermalinksModel::SLUG->value => $slug, + WorkPermalinksModel::ANCHOR->value => $slug + ]); + + return $create->ok; + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to get work data, please try again later", 503); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + // Generate URL slug from title text or UUID if undefined + $slug = !empty($_POST["title"]) ? self::gen_slug($_POST["title"]) : parent::gen_uuid4(); + + // Check if an entity already exists with slugified title from GET endpoint + $existing_entity = Call("work?id={$slug}", Method::GET); + // Response is not 404 (Not found) so we can't create the entity + if ($existing_entity->code !== 404) { + // Response is not a valid entity, something went wrong + if ($existing_entity->code !== 200) { + return $this->resp_database_error(); + } + + // Return 402 Conflict + return new Response("Entity with id '{$slug}' already exists", 402); + } + + // Get created timestamp from payload or use current time if not specified + $created_timestamp = $_POST[WorkModel::DATE_TIMESTAMP_CREATED->value] + ? $_POST[WorkModel::DATE_TIMESTAMP_CREATED->value] + : time(); + + // Attempt to create new entity + $insert = $this->db->for(WorkModel::TABLE) + ->insert([ + WorkModel::ID->value => $slug, + WorkModel::TITLE->value => $_POST["title"], + WorkModel::SUMMARY->value => $_POST["summary"], + WorkModel::IS_LISTABLE->value => true, + WorkModel::IS_READABLE->value => true, + WorkModel::DATE_YEAR->value => date("Y", $created_timestamp), + WorkModel::DATE_MONTH ->value => date("n", $created_timestamp), + WorkModel::DATE_DAY->value => date("j", $created_timestamp), + WorkModel::DATE_TIMESTAMP_MODIFIED->value => null, + WorkModel::DATE_TIMESTAMP_CREATED->value => $created_timestamp, + ]); + + // Bail out if insert failed + if (!$insert) { + return $this->resp_database_error(); + } + + // Create permalink for new entity + if (!$this->create_permalink($slug)) { + // Rollback created entity if permalink creation failed + Call("work", Method::DELETE, [WorkModel::ID->value => $slug]); + + return new Response("Failed to create permalink", 500); + } + + // Return 201 Created and entity slug as body + return new Response($slug, 201); + } + } \ No newline at end of file diff --git a/api/endpoints/work/actions/DELETE.php b/api/endpoints/work/actions/DELETE.php new file mode 100755 index 0000000..2eb1e82 --- /dev/null +++ b/api/endpoints/work/actions/DELETE.php @@ -0,0 +1,73 @@ +ruleset = new Ruleset(strict: true); + + $this->ruleset->POST([ + (new Rules("id")) + ->required() + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ]); + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to get work data, please try again later", 503); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + // Ensure the action exists by id + $existing_action = $this->db->for(WorkActionsModel::TABLE) + ->where([ + WorkActionsModel::ID->value => $_POST["id"] + ]) + ->select(null); + + // Return idempotent deletion if the action does not exist + if ($existing_action->num_rows === 0) { + return new Response($_POST["id"]); + } + + // Attempt to delete action by id + $delete = $this->db->for(WorkActionsModel::TABLE) + ->delete([ + WorkActionsModel::ID->value => $_POST["id"] + ]); + + // Return 201 Created and entity id as body if insert was successful + return $delete === true ? new Response($_POST["id"], 201) : $this->resp_database_error(); + } + } \ No newline at end of file diff --git a/api/endpoints/work/actions/POST.php b/api/endpoints/work/actions/POST.php new file mode 100755 index 0000000..acca855 --- /dev/null +++ b/api/endpoints/work/actions/POST.php @@ -0,0 +1,102 @@ +ruleset = new Ruleset(strict: true); + + $this->ruleset->GET([ + (new Rules("id")) + ->required() + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ]); + + $this->ruleset->POST([ + (new Rules(WorkActionsModel::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) + ->max(4) + ->default([]), + + (new Rules(WorkActionsModel::EXTERNAL->value)) + ->type(Type::BOOLEAN) + ->default(false) + ]); + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to get work data, please try again later", 503); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + // Ensure an entity with the provided id exists + $entity = Call("work?id={$_GET["id"]}", Method::GET); + if ($entity->code !== 200) { + // Response from endpoint is not 404, something went wrong + if ($entity->code !== 404) { + return $this->resp_database_error(); + } + + return new Response("No entity with id '{$_GET["id"]}' was found", 404); + } + + // Attempt to create action for entity + $insert = $this->db->for(WorkActionsModel::TABLE) + ->insert([ + WorkActionsModel::ID->value => parent::gen_uuid4(), + WorkActionsModel::ANCHOR->value => $_GET["id"], + WorkActionsModel::DISPLAY_TEXT->value => $_POST[WorkActionsModel::DISPLAY_TEXT->value], + WorkActionsModel::HREF->value => $_POST[WorkActionsModel::HREF->value], + WorkActionsModel::CLASS_LIST->value => implode(",", $_POST[WorkActionsModel::CLASS_LIST->value]), + WorkActionsModel::EXTERNAL->value => $_POST[WorkActionsModel::EXTERNAL->value], + ]); + + // Return 201 Created and entity id as body if insert was successful + return $insert === true ? new Response($_GET["id"], 201) : $this->resp_database_error(); + } + } \ No newline at end of file diff --git a/api/endpoints/work/permalinks/GET.php b/api/endpoints/work/permalinks/GET.php new file mode 100755 index 0000000..9fe437b --- /dev/null +++ b/api/endpoints/work/permalinks/GET.php @@ -0,0 +1,60 @@ +ruleset = new Ruleset(strict: true); + + $this->ruleset->GET([ + (new Rules("id")) + ->required() + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ]); + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to resolve permalink, please try again later", 503); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + // Get all anchors that match the requested slug + $resolve = $this->db->for(WorkPermalinksModel::TABLE) + ->where([WorkPermalinksModel::SLUG->value => $_GET["id"]]) + ->select(WorkPermalinksModel::ANCHOR->value); + + // Return array of all matched work table ids. Or empty array if none found + return parent::is_mysqli_result($resolve) + ? new Response(array_column($resolve->fetch_all(MYSQLI_ASSOC), WorkPermalinksModel::ANCHOR->value)) + : $this->resp_database_error(); + } + } \ No newline at end of file diff --git a/api/endpoints/work/permalinks/POST.php b/api/endpoints/work/permalinks/POST.php new file mode 100755 index 0000000..6425316 --- /dev/null +++ b/api/endpoints/work/permalinks/POST.php @@ -0,0 +1,83 @@ +ruleset = new Ruleset(strict: true); + + $this->ruleset->POST([ + (new Rules("slug")) + ->required() + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), + + (new Rules("anchor")) + ->required() + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ]); + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to resolve permalink, please try again later", 503); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + // Check if an entity exists with slug + $existing_entity = Call("work?id={$_POST["slug"]}", Method::GET); + // Response is not 404 (Not found) so we can't create the entity + if ($existing_entity->code !== 200) { + // Response is not a valid entity, something went wrong + if ($existing_entity->code !== 404) { + return $this->resp_database_error(); + } + + // Return 402 Conflict + return new Response("No work entity with id '{$_POST["slug"]}' was found to permalink", 404); + } + + // Attempt to create new entity + $insert = $this->db->for(WorkPermalinksModel::TABLE) + ->insert([ + WorkPermalinksModel::SLUG->value => $_POST["slug"], + WorkPermalinksModel::ANCHOR->value => $_POST["anchor"], + WorkPermalinksModel::DATE_TIMESTAMP_CREATED->value => time(), + ]); + + // Return 201 Created and entity slug as body if insert was successful + return $insert === true ? new Response($_POST["slug"], 201) : $this->resp_database_error(); + } + } \ No newline at end of file diff --git a/api/endpoints/work/tags/DELETE.php b/api/endpoints/work/tags/DELETE.php new file mode 100755 index 0000000..f674469 --- /dev/null +++ b/api/endpoints/work/tags/DELETE.php @@ -0,0 +1,80 @@ +ruleset = new Ruleset(strict: true); + + $this->ruleset->POST([ + (new Rules("id")) + ->required() + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH), + + (new Rules(WorkTagsModel::NAME->value)) + ->required() + ->type(Type::ENUM, WorkTagsNameEnum::names()) + ]); + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to get work data, please try again later", 503); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + // Ensure the tag exists for entity id + $existing_tag = $this->db->for(WorkTagsModel::TABLE) + ->where([ + WorkTagsModel::ANCHOR->value => $_POST["id"], + WorkTagsModel::NAME->value => $_POST["name"] + ]) + ->select(null); + + // Return idempotent deletion if the tag does not exist + if ($existing_tag->num_rows === 0) { + return new Response($_POST["id"]); + } + + // Attempt to delete tag for entity + $delete = $this->db->for(WorkTagsModel::TABLE) + ->delete([ + WorkTagsModel::ANCHOR->value => $_POST["id"], + WorkTagsModel::NAME->value => $_POST["name"] + ]); + + // Return 201 Created and entity id as body if insert was successful + return $delete === true ? new Response($_POST["id"], 201) : $this->resp_database_error(); + } + } \ No newline at end of file diff --git a/api/endpoints/work/tags/POST.php b/api/endpoints/work/tags/POST.php new file mode 100755 index 0000000..d78c03c --- /dev/null +++ b/api/endpoints/work/tags/POST.php @@ -0,0 +1,93 @@ +ruleset = new Ruleset(strict: true); + + $this->ruleset->GET([ + (new Rules("id")) + ->required() + ->type(Type::STRING) + ->min(1) + ->max(parent::MYSQL_VARCHAR_MAX_LENGTH) + ]); + + $this->ruleset->POST([ + (new Rules(WorkTagsModel::NAME->value)) + ->required() + ->type(Type::ENUM, WorkTagsNameEnum::names()) + ]); + } + + // # Responses + + // Return 422 Unprocessable Content error if request validation failed + private function resp_rules_invalid(): Response { + return new Response($this->ruleset->get_errors(), 422); + } + + // Return a 503 Service Unavailable error if something went wrong with the database call + private function resp_database_error(): Response { + return new Response("Failed to get work data, please try again later", 503); + } + + public function main(): Response { + // Bail out if request validation failed + if (!$this->ruleset->is_valid()) { + return $this->resp_rules_invalid(); + } + + // Ensure an entity with the provided id exists + $entity = Call("work?id={$_GET["id"]}", Method::GET); + if ($entity->code !== 200) { + // Response from endpoint is not 404, something went wrong + if ($entity->code !== 404) { + return $this->resp_database_error(); + } + + return new Response("No entity with id '{$_GET["id"]}' was found", 404); + } + + // Ensure the tag does not already exist for entity + $existing_tag = $this->db->for(WorkTagsModel::TABLE) + ->where([ + WorkTagsModel::ANCHOR->value => $_GET["id"], + WorkTagsModel::NAME->value => $_POST["name"] + ]) + ->select(null); + + // Bail out if this tag already exists + if ($existing_tag->num_rows !== 0) { + return new Response("Tag '{$_POST["name"]}' is already set on entity id '{$_GET["id"]}'", 402); + } + + // Attempt to create tag for entity + $insert = $this->db->for(WorkTagsModel::TABLE) + ->insert([ + WorkTagsModel::ANCHOR->value => $_GET["id"], + WorkTagsModel::NAME->value => $_POST["name"] + ]); + + // Return 201 Created and entity id as body if insert was successful + return $insert === true ? new Response($_GET["id"], 201) : $this->resp_database_error(); + } + } \ No newline at end of file diff --git a/api/src/databases/VLWdb.php b/api/src/databases/VLWdb.php new file mode 100755 index 0000000..bb1c4ca --- /dev/null +++ b/api/src/databases/VLWdb.php @@ -0,0 +1,50 @@ +db = new MySQL( + $_ENV["vlwdb"]["mariadb_host"], + $_ENV["vlwdb"]["mariadb_user"], + $_ENV["vlwdb"]["mariadb_pass"], + $_ENV["vlwdb"]["mariadb_db"], + ); + } + + // 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) + ); + } + + public static function is_mysqli_result(\mysqli_result|bool $resp): bool { + return $resp instanceof \mysqli_result; + } + } \ No newline at end of file diff --git a/api/src/databases/models/Coffee.php b/api/src/databases/models/Coffee.php new file mode 100755 index 0000000..0d4377c --- /dev/null +++ b/api/src/databases/models/Coffee.php @@ -0,0 +1,10 @@ + a { + --underline-tickness: 3px; + + display: initial; + text-decoration: underline; + text-decoration-thickness: var(--underline-tickness); + text-underline-offset: var(--underline-tickness); + text-decoration-color: var(--color-accent); +} + +/* ## Buttons */ + +button { + font-size: inherit; + padding: calc(var(--padding) / 2) var(--padding); + color: white; + border: solid 2px white; + border-radius: 6px; + background-color: transparent; + cursor: pointer; +} + +button.solid { + color: black; + border-color: var(--color-accent); + background-color: var(--color-accent); +} + +a > button::after { + content: " ➜"; +} + +a[target="_blank"] > button::after, +:is(h1, h2, h3, p, li) > a[target="_blank"]::after { + content: " ⮥"; + color: var(--color-accent); + white-space: nowrap; +} + +a > button.solid:not(:hover)::after { + color: black; +} + +/* ## Header */ + +header { + --border-style: solid 1px rgba(255, 255, 255, .2); + + position: sticky; + top: 0; + width: 100%; + height: var(--running-size); + border-bottom: var(--border-style); + display: grid; + align-items: stretch; + justify-items: end; + grid-template-columns: 1fr var(--running-size); + background-color: rgba(0, 0, 0, .8); + z-index: 100; + -webkit-backdrop-filter: blur(20px); + backdrop-filter: blur(20px); +} + +header nav { + display: flex; + align-items: center; + padding: var(--padding); +} + +header .logo { + width: calc(var(--running-size) - 1px); + height: calc(var(--running-size) - 1px); + display: grid; + align-items: center; + justify-items: center; + border-left: var(--border-style); +} + +header .logo path.stroke { + fill: var(--color-accent); +} + +header searchbox { + display: none; +} + +/* ## Main */ + +main { + transition: 400ms transform; + position: relative; + padding: calc(var(--padding) * 1.5); + width: 100%; + max-width: 1000px; +} + +main > * { + transition: 100ms opacity; + opacity: 1; +} + +main.loading > * { + opacity: 0; +} + +/* ## Search */ + +/* ### Box */ + +searchbox { + display: grid; + width: 100%; + border-left: var(--border-style); + grid-template-columns: 25px 1fr; + align-items: center; + padding: var(--padding); + gap: var(--padding); + fill: var(--color-accent); + font-size: 14px; + color: rgba(255, 255, 255, .5); + cursor: pointer; +} + +/* ### Dialog */ + +body.search-dialog-open main { + transform: scale(.94); +} + +dialog.search { + transition: 200ms height cubic-bezier(.41,0,.34,.99); + margin: auto; + width: 100%; + max-width: 1000px; + height: calc(var(--running-size) + (var(--padding) * 5)); + max-height: 1000px; + border-color: transparent; + background-color: transparent; + overflow: visible; + outline: none; +} + +dialog.search.active { + height: 70vh; +} + +dialog.search search { + transition: 400ms transform, 200ms opacity; + width: 100%; + height: 100%; + display: grid; + grid-template-rows: var(--running-size) 1fr; + gap: calc(var(--padding) * 2); + transform: scale(1.1); + overflow: hidden; + background-color: rgba(255, 255, 255, .05); + -webkit-backdrop-filter: blur(20px); + backdrop-filter: brightness(.3) blur(20px); + border-radius: 12px; + box-shadow: 0 10px 30px 10px black; + opacity: 0; +} + +body.search-dialog-open dialog.search search { + transform: scale(1); + padding: calc(var(--padding) * 1.5); + opacity: 1; +} + +search input { + transition: 200ms background-color, 200ms box-shadow, 200ms color; + border-radius: 6px; + border: none; + outline: none; + color: black; + font-size: 18px; + padding: var(--padding) calc(var(--padding) * 1.5); + background-color: rgba(255, 255, 255, .05); + box-shadow: 0 5px 70px 10px rgba(0, 0, 0, .3); + color: white; +} + +search input:focus { + background-color: rgba(255, 255, 255, .9); + box-shadow: 0 10px 30px 10px black; + color: black; +} + +/* ### Search results */ + +dialog.search search search-results { + overflow-y: auto; +} + +dialog.search search search-results > svg { + margin: auto; + width: 150px; + fill: rgba(255, 255, 255, .05); +} + +/* # Feature queries */ + +@media (hover: hover) { + :is(h1, h2, h3, p, li) > a:hover { + text-underline-offset: 1px; + text-decoration-thickness: calc(var(--underline-tickness) * 2); + color: var(--color-accent); + } + + /* # Components */ + + button { + transition: 200ms background-color, 200ms border-color, 200ms color; + } + + button:hover { + border-color: rgba(255, 255, 255, .2); + background-color: rgba(255, 255, 255, .1); + } + + button.solid:hover { + color: var(--color-accent); + border-color: rgba(var(--primer-color-accent), .2); + background-color: rgba(var(--primer-color-accent), .2); + box-shadow: 0 -10px 20px 10px rgba(var(--primer-color-accent), .05); + } + + /* ## Header */ + + header .logo:hover path.solid { + fill: var(--color-accent); + } + + searchbox { + transition: 200ms background-color; + } + + searchbox:hover { + background-color: rgba(255, 255, 255, .07); + } +} + +/* # Size queries */ + +@media (min-width: 700px) { + header { + grid-template-columns: 1fr 250px var(--running-size); + } + + header nav { + justify-self: start; + margin: 0 calc(var(--padding) / 2); + } + + /* # Menu */ + + /* < Move the search box to the header */ + header searchbox { + display: grid; + } + + menu searchbox { + display: none; + } + /* /> */ +} \ No newline at end of file diff --git a/assets/css/fonts.css b/assets/css/fonts.css new file mode 100755 index 0000000..127c387 --- /dev/null +++ b/assets/css/fonts.css @@ -0,0 +1,16 @@ +@font-face { + font-family: "Roboto Mono"; + ascent-override: 100%; + font-weight: 400; + size-adjust: 105%; + font-stretch: 97.5% 112.5%; + src: local("Roboto Mono Regular"), url("/assets/fonts/roboto-mono-regular.woff2") format("woff2"); +} + +@font-face { + font-family: "Roboto Mono"; + ascent-override: 100%; + size-adjust: 95%; + font-weight: 800; + src: local("Roboto Mono Bold"), url("/assets/fonts/roboto-mono-bold.woff2") format("woff2"); +} \ No newline at end of file diff --git a/assets/css/pages/about.css b/assets/css/pages/about.css new file mode 100755 index 0000000..e1fdcc2 --- /dev/null +++ b/assets/css/pages/about.css @@ -0,0 +1,105 @@ +/* # Overrides */ + +:root { + --primer-color-accent: 148, 255, 21; + --color-accent: rgb(var(--primer-color-accent)); +} + +main { + display: flex; + flex-direction: column; + gap: var(--padding); +} + +/* # Sections */ + +/* ## Into */ + +section.intro h2 { + font-size: 25px; +} + +section.intro h1 { + font-size: 40px; + color: var(--color-accent); +} + +/* ## Divider */ + +main > hr { + border-color: rgba(255, 255, 255, .1); +} + +/* ## About */ + +section.about { + display: flex; + flex-direction: column; + gap: calc(var(--padding) / 2); +} + +section.about { + font-size: 16px; +} + +section.about p:first-of-type:first-letter { + font-size: 1.5rem; + 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; +} + +/* ## Version */ + +section.version { + color: rgba(255, 255, 255, .2); +} + +/* # Interests */ + +div.interests { + transition: 300ms opacity; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + font-weight: bold; + pointer-events: none; + font-size: 60px; + color: var(--color-accent); + overflow: hidden; + opacity: 0; + z-index: 200; +} + +div.interests.active { + opacity: 1; +} + +div.interests p { + --text-shadow-blur: 30px; + + transition: 300ms transform; + 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); + } +} \ No newline at end of file diff --git a/assets/css/pages/contact.css b/assets/css/pages/contact.css new file mode 100755 index 0000000..88ef8a5 --- /dev/null +++ b/assets/css/pages/contact.css @@ -0,0 +1,184 @@ +/* # Overrides */ + +:root { + --primer-color-accent: 255, 195, 255; + --color-accent: rgb(var(--primer-color-accent)); +} + +main { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--padding); +} + +/* # Sections */ + +main > svg { + margin: var(--padding) 0; +} + +/* ## Social */ + +section.social { + --icon-size: 60px; + + display: grid; + grid-template-columns: repeat(3, var(--icon-size)); + grid-template-rows: var(--icon-size); + align-items: center; + fill: white; + gap: var(--padding); +} + +section.social social { + transition: 200ms fill; + position: relative; +} + +/* ### Hover tooltip */ + +section.social social p { + display: none; + position: absolute; + top: 0; + left: 0; + transform: translate(0, 0); + background-color: rgba(var(--primer-color-accent), .1); + padding: 5px 10px; + font-size: 17px; + white-space: nowrap; + pointer-events: none; + border-radius: 6px; + -webkit-backdrop-filter: brightness(.2) blur(20px); + backdrop-filter: brightness(.2) blur(20px); +} + +section.social social:hover { + fill: var(--color-accent); +} + +section.social social.hovering p { + display: initial; +} + +/* ## OpenPGP key */ + +section.pgp { + max-width: 800px; + position: relative; + text-align: center; + background-color: rgba(var(--primer-color-accent), .15); + padding: calc(var(--padding) * 1.5); + transform: rotate(-1.5deg); +} + +section.pgp > svg { + position: absolute; + top: -30px; + right: -20px; + width: 60px; + fill: var(--color-accent); +} + +section.pgp > p { + margin-bottom: var(--padding); + padding: var(--padding); +} + +section.pgp .buttons { + display: flex; + flex-direction: column; + gap: var(--padding); +} + +/* ## Contact form */ + +section.form :is(input, textarea) { + min-width: 100%; + max-width: 100%; + color: black; + font-size: 15px; + padding: var(--padding); + border-radius: 4px; + border: none; + outline: none; +} + +section.form input { + height: calc(var(--running-size) - var(--padding)); +} + +section.form textarea { + min-height: calc(var(--running-size) * 1.5); +} + +section.form { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: var(--padding); + width: 100%; +} + +section.form form { + display: contents; +} + +section.form input-group { + width: 100%; + display: flex; + flex-direction: column; + gap: calc(var(--padding) / 2); +} + +section.form input-group label { + color: var(--color-accent); +} + +section.form button { + width: 100%; + max-width: 500px; +} + +/* ### Contact form messages */ + +section.form-message { + width: 100%; + display: flex; + flex-direction: column; + gap: calc(var(--padding) / 2); + padding: var(--padding); + background-color: white; + margin: var(--padding) 0; + color: black; +} + +section.form-message h3 { + text-align: center; +} + +section.form-message pre { + font-size: 11px; + padding: var(--padding); + background-color: rgba(0, 0, 0, .15); +} + +section.form-message.error { + background-color: #ec4444; + color: white; +} + +section.form-message.sent { + background-color: var(--color-accent); +} + +/* # Size queries */ + +@media (min-width: 460px) { + section.pgp .buttons { + flex-direction: row; + justify-content: center; + } +} \ No newline at end of file diff --git a/assets/css/pages/error.css b/assets/css/pages/error.css new file mode 100755 index 0000000..5dd4d2a --- /dev/null +++ b/assets/css/pages/error.css @@ -0,0 +1,51 @@ +/* # Overrides */ + +header { + background-color: transparent; + -webkit-backdrop-filter: unset; + backdrop-filter: unset; +} + +main { + max-width: unset; + display: grid; + justify-items: center; +} + +/* # Glitch effects */ + +/* ## Canvas */ + +canvas { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + opacity: .3; + transform: scale(1.3); +} + +/* ## Text */ + +[glitch-text] { + transition: 50ms text-shadow; +} + +/* # Sections */ + +section.error h1 { + font-size: 30vw; + user-select: none; + animation: rumble 100ms infinite linear; + opacity: .2; +} + +section.error h1 span { + +} + +@keyframes rumble { + to { transform: translateX(1px); } +} \ No newline at end of file diff --git a/assets/css/pages/index.css b/assets/css/pages/index.css new file mode 100755 index 0000000..777db69 --- /dev/null +++ b/assets/css/pages/index.css @@ -0,0 +1,170 @@ +/* # Main styles */ + +/* ## Picture */ + +main { + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: column-reverse; +} + +main img { + margin: auto; + width: 25vh; + pointer-events: none; + -webkit-filter: hue-rotate(var(--hue-accent)); + filter: hue-rotate(var(--hue-accent)); +} + +/* ## Menu */ + +.menu { + width: 100%; + max-width: 300px; + display: flex; + flex-direction: column; +} + +.menu menu { + margin: var(--padding) 0; + list-style: none; + padding: unset; + text-align: right; + font-size: clamp(20px, 8vh, 60px); + font-weight: 900; + line-height: clamp(20px, 8vh, 60px); + color: var(--color-accent); +} + +.menu menu li { + transition: 200ms opacity, 200ms color; +} + +.menu svg { + width: 100%; +} + +/* ### Copy email button */ + +.menu button { + text-align: right; + border: unset; + padding: var(--padding) 0; +} + +.menu button p:first-of-type { + color: var(--color-accent); +} + +/* # Email-copied splash */ + +splash { + --confetti: unset; + --text-shadow: 0 0 30px black; + + display: initial !important; + transition: 300ms opacity; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 80px; + color: white; + z-index: 200; + font-weight: 900; + pointer-events: none; + perspective: 300px; + text-shadow: + var(--text-shadow), + var(--text-shadow), + var(--text-shadow), + var(--text-shadow) + ; + animation: splash-reveal 1s ease; +} + +splash.hide { + opacity: 0; +} + +splash::after { + content: ""; + top: 50%; + left: 50%; + position: absolute; + width: 8px; + height: 16px; + background-color: transparent; + box-shadow: var(--confetti); + animation: splash-confetti 1s ease; + opacity: 0; +} + +/* ## Keyframes */ + +@keyframes splash-confetti { + 0% { + transform: rotate(12deg) scale(0); + opacity: 1; + } + + 60% { + opacity: 1; + } + + 100% { + transform: rotate(-10deg) scale(1); + opacity: 0; + } +} + +@keyframes splash-reveal { + 0% { transform: translate(-50%, -50%) rotate(-8deg) scale(0); } + 35% { transform: translate(-50%, -50%) rotate(-3deg) scale(1.1); } + 100% { transform: translate(-50%, -50%) rotate(0deg) scale(1); } +} + +/* # Features */ + +.cta::before { + content: "tap "; +} + +@media (pointer: fine) { + .cta::before { + content: "click "; + } +} + +/* # Feature queries */ + +@media (hover: hover) { + .menu menu:hover li { + opacity: .6; + } + + .menu menu li:hover { + opacity: 1; + text-shadow: 0 0 10px rgba(var(--primer-color-accent), .4); + } +} + +/* # Size quries */ + +@media (min-width: 900px) { + main { + display: grid; + grid-template-columns: repeat(2, 1fr); + justify-items: center; + align-items: center; + } + + main img { + width: 35vh; + } + + button:hover { + background-color: transparent; + } +} diff --git a/assets/css/pages/search.css b/assets/css/pages/search.css new file mode 100755 index 0000000..4067eab --- /dev/null +++ b/assets/css/pages/search.css @@ -0,0 +1,94 @@ +/* # Overrides */ + +[vv-page="/search"]:not(body) { + display: flex; + flex-direction: column; + gap: var(--padding); +} + +/* # Sections */ + +/* ## Search */ + +section.search { + width: 100%; + display: flex; + 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); +} + +section.search form { + display: contents; +} + +section.search search { + width: 100%; +} + +section.search input { + width: 100%; +} + +section.search button[type="submit"] { + width: 100%; + max-width: 350px; +} + +body:not([vv-page="/search"]) section.search { + display: none; +} + +/* # Search results */ + +main > svg, +dialog.search search search-results > svg { + margin: auto; + width: 150px; + fill: rgba(255, 255, 255, .05); +} + +dialog.search search search-results .empty { + text-align: center; +} + +/* ## Titles */ + +section.title 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, .1); + border-radius: 6px; +} + +/* # Feature queries */ + +@media (hover: hover) { + section.results.work .result { + transition: 300ms background-color; + } + + section.results.work .result:hover { + background-color: rgba(255, 255, 255, .2); + box-shadow: 0 5px 70px 10px rgba(0, 0, 0, .3); + } +} \ No newline at end of file diff --git a/assets/css/pages/work.css b/assets/css/pages/work.css new file mode 100755 index 0000000..2bfd42b --- /dev/null +++ b/assets/css/pages/work.css @@ -0,0 +1,207 @@ +/* # Overrides */ + +:root { + --primer-color-accent: 3, 255, 219; + --color-accent: rgb(var(--primer-color-accent)); +} + +main { + display: flex; + flex-direction: column; + gap: var(--padding); + padding: calc(var(--padding) * 1.5); + width: 100%; + max-width: 1200px; + overflow-x: initial; +} + +/* # Sections */ + +/* ## Git */ + +section.git { + display: flex; + flex-direction: column; + gap: var(--padding); + background-color: rgba(var(--primer-color-accent), .1); + padding: calc(var(--padding) * 1.5); + border-radius: 6px; +} + +section.git svg { + width: 60px; +} + +section.git .buttons { + display: flex; + flex-direction: column; + gap: var(--padding); +} + +/* ## Timeline */ + +section.timeline { + --timestamp-gap: calc(var(--padding) / 2); + + width: 100%; +} + +section.timeline :is(.year, .month, .day) { + display: grid; + grid-template-columns: calc(40px + var(--timestamp-gap)) 1fr; + grid-template-rows: 1fr; +} + +section.timeline .track { + --opacity: .15; + --width: 2%; + + background: linear-gradient(90deg, + transparent 0%, transparent calc(50% - var(--width)), + rgba(255, 255, 255, var(--opacity)) calc(50% - var(--width)), rgba(255, 255, 255, var(--opacity)) calc(50% + var(--width)), + transparent calc(50% + var(--width)), transparent 100% + ); +} + +section.timeline .track p { + position: sticky; + top: calc(var(--running-size) + var(--padding)); + padding: calc(var(--padding) / 2) 0; + background-color: black; + color: var(--color-accent); +} + +section.timeline :not(.year) > .track p::before { + content: "/ "; + color: rgba(255, 255, 255, .3); +} + +/* ### Item */ + +section.timeline .items .item { + display: flex; + flex-direction: column; + gap: calc(var(--padding) / 2); + padding: var(--padding); +} + +section.timeline .items .item + .item { + border-top: solid 2px rgba(255, 255, 255, .2); +} + +section.timeline .items .item:first-of-type { + margin-top: var(--padding); + border-top: solid 2px var(--color-accent); +} + +/* No border style for the latest item (from the top) in the list */ +section.timeline .year:first-of-type .month:first-of-type .day:first-of-type .items .item:first-of-type { + margin-top: unset; + border-top: unset; +} + +section.timeline .items .item .tags { + display: flex; + gap: calc(var(--padding) / 2); +} + +section.timeline .items .item .tags .tag { + font-size: 11px; + letter-spacing: 1px; + color: rgba(255, 255, 255, .7); + background-color: rgba(255, 255, 255, .15); + border-radius: 4px; + padding: 5px 10px; +} + +section.timeline .items .item h2 { + font-size: 30px; +} + +section.timeline .items .item h3 { + font-size: 25px; +} + +section.timeline .items .item p { + font-size: 16px; +} + +section.timeline .items .item img { + max-width: 100%; + height: 250px; +} + +section.timeline .items .item .actions { + margin-top: 7px; + font-size: 13px; +} + +/* ## Note */ + +section.note { + text-align: center; +} + +/* # Size queries */ + +@media (min-width: 460px) { + section.git .buttons { + flex-direction: row; + } +} + +@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; + } + + section.timeline .track { + position: relative; + left: calc(var(--padding) * 1.5); + background: unset; + z-index: 10; + pointer-events: none; + } + + section.timeline .track p { + background-color: black; + } + + section.timeline :is(.years, .year, .months, .month, .days, .day) { + width: 0; + } + + section.timeline .items { + position: relative; + left: -140px; + } + + section.timeline .items .item { + width: calc(100vw - (var(--padding) * 3.5)); + } + + section.timeline .items .item:first-of-type { + border-top-color: rgba(var(--primer-color-accent), .2); + } + + section.timeline .year:first-of-type .month:first-of-type .day:first-of-type .items .item:first-of-type { + margin-top: var(--padding); + } +} \ No newline at end of file diff --git a/assets/fonts/roboto-mono-bold.woff2 b/assets/fonts/roboto-mono-bold.woff2 new file mode 100755 index 0000000000000000000000000000000000000000..1b7b9bbf1e66767c17979cfa10bf458720ad1643 GIT binary patch literal 40812 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 M|Mw4zqyhi{0FPx46aWAK literal 0 HcmV?d00001 diff --git a/assets/fonts/roboto-mono-regular.woff2 b/assets/fonts/roboto-mono-regular.woff2 new file mode 100755 index 0000000000000000000000000000000000000000..0ab4f8b283c6b930614864884dd8f00806f9ae33 GIT binary patch literal 40704 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

?pmBM%e|5ec}G@rPOl$*kyjBtFXheM(>YVC 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 I`>*)K3Hov=u>b%7 literal 0 HcmV?d00001 diff --git a/assets/js/document.js b/assets/js/document.js new file mode 100755 index 0000000..8405b26 --- /dev/null +++ b/assets/js/document.js @@ -0,0 +1,72 @@ +new vv.Interactions("document"); + +const mainElement = document.querySelector(vv._env.MAIN); + +// Crossfade pages on navigation +// Or maybe I shouldn't... hmmm +mainElement.addEventListener(vv.Navigation.events.LOADING, () => { + mainElement.classList.add("loading"); + + // Clean up modified transform-origin if set after search dialog animation + mainElement.style.removeProperty("transform-origin"); +}); + +mainElement.addEventListener(vv.Navigation.events.LOADED, () => { + [...document.querySelectorAll("dialog")].forEach(element => element.close()) + + // Wait 200ms for the page fade-in animation to finish + setTimeout(() => mainElement.classList.remove("loading"), 200); +}); + +// Search dialog open/close logic +{ + const CLASNAME_DIALOG_OPEN = "search-dialog-open"; + // Offset in pixels from scroll position when scaling the main element + const TRANSFORM_ORIGIN_Y_PADDING = 350; + + const dialog = document.querySelector("dialog.search"); + + // "Polyfill" for HTMLDialogELement open and close events + (new MutationObserver((mutations) => { + // There is only one search dialog elemenet + const target = mutations[0].target; + + // Set or unset dialog open class on body depending on dialog visibility + target.hasAttribute("open") + ? target.dispatchEvent(new Event("open")) + : target.dispatchEvent(new Event("close")); + + }).observe(dialog, { attributes: true })); + + dialog.addEventListener("open", () => { + // Scale main element from the current scroll position + mainElement.style.setProperty("transform-origin", `50% calc(${window.scrollY}px + ${TRANSFORM_ORIGIN_Y_PADDING}px)`); + + document.body.classList.add(CLASNAME_DIALOG_OPEN); + }); + dialog.addEventListener("close", () => document.body.classList.remove(CLASNAME_DIALOG_OPEN)); + + // Close search dialog if dialog is clicked outside inner content + dialog.addEventListener("click", (event) => event.target === dialog ? dialog.close() : null); + + // Open search dialog when searchbox is clicked + document.querySelector("searchbox").addEventListener("click", () => dialog.showModal()); +} + +// Search logic +{ + const searchResultsElement = document.querySelector("search-results"); + const search = (query) => { + new vv.Navigation(`/search?q=${query}`, { + carrySearchParams: true + }).navigate(searchResultsElement); + }; + + // Run search on keyup + document.querySelector("search input").addEventListener("keyup", (event) => search(event.target.value)); + + // Trigger expand search box animation + document.querySelector("search input").addEventListener("keydown", () => { + searchResultsElement.closest("dialog").classList.add("active"); + }, { once: true }); +} \ No newline at end of file diff --git a/assets/js/modules/glitch/Generator.mjs b/assets/js/modules/glitch/Generator.mjs new file mode 100755 index 0000000..07761b9 --- /dev/null +++ b/assets/js/modules/glitch/Generator.mjs @@ -0,0 +1,79 @@ +// Fetch and create glitchy background effects +class Generator { + constructor() { + this.bg = { + _this: this, + _image: null, + _dir: location, + _dir_rel: "assets/media/glitch_b64/", + count: 4, + // Get or set current background + get current () { return this._image; }, + set current (image) { + this._image = image; + this._this.setBg(image); + }, + // Get or set the path to where base64 images are stored + get dir () { return this._dir; }, + set dir (newPath) { + const url = new URL(newPath); + + // Replace pathname of this file with relative path to assets + const path = url.pathname.split("/"); + path[path.length - 1] = this._dir_rel; + + url.pathname = path.join("/"); + this._dir = url.toString(); + } + } + } + + // Genrate random int in range + static randInt(min, max) { + if(min === max) return min; + return Math.round(Math.random() * (max - min) + min); + } + + // Generate random string of length from charset + static randStr(length = 2) { + const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let output = ""; + for(let i = 0; i < length; i++) { + output += charset.charAt(Math.floor(Math.random() * charset.length)); + } + return output; + } + + // Give generated background image to parent thread + setBg(image) { + if(typeof image !== "string") throw new TypeError("Image must be of type 'string'"); + postMessage(["BG_UPDATE", image]); + } + + // Generate and set a glitchy image + glitch() { + if(!this.bg.current) return; + const image = this.bg.current.replaceAll(Generator.randStr(), Generator.randStr()); + this.setBg(image); + } + + // Fetch a base64 encoded background image + async fetchBg(id) { + const url = new URL(this.bg.dir); + + url.pathname += id + ".txt"; + + const image = await fetch(url); + if(!image.ok) throw new Error("Failed to fetch background image"); + + return image.text(); + } + + // Load a random background from the image set + async randBg() { + const id = Generator.randInt(1, this.bg.count); + + const image = await this.fetchBg(id); + this.bg.current = image; + } +} \ No newline at end of file diff --git a/assets/js/modules/glitch/Glitch.mjs b/assets/js/modules/glitch/Glitch.mjs new file mode 100755 index 0000000..dd0de47 --- /dev/null +++ b/assets/js/modules/glitch/Glitch.mjs @@ -0,0 +1,41 @@ +export default class Glitch { + constructor(target) { + this.worker = new Worker(this.getWorkerScriptURL()); + this.worker.addEventListener("message", event => this.message(event)); + + this.target = target ? target : document.body; + } + + // Update the target CSS background with an image URL + setVisibleBg(image) { + this.target.style.setProperty("background-image", `url(${image})`); + } + + // Get URL for the dedicated worker + getWorkerScriptURL() { + const name = "GlitchWorker.js"; + const url = new URL(import.meta.url); + + // Replace pathname of this file with worker + const path = url.pathname.split("/"); + path[path.length - 1] = name; + + url.pathname = path.join("/"); + return url.toString(); + } + + // Event handler for messages from worker thread + message(event) { + const data = typeof event.data === "object" ? event.data : [event.data]; + + switch(data[0]) { + case "READY": + this.worker.postMessage(["START", new URL(location).toString()]); + break; + + case "BG_UPDATE": + this.setVisibleBg(data[1]); + break; + } + } +} \ No newline at end of file diff --git a/assets/js/modules/glitch/GlitchWorker.js b/assets/js/modules/glitch/GlitchWorker.js new file mode 100755 index 0000000..4b52dff --- /dev/null +++ b/assets/js/modules/glitch/GlitchWorker.js @@ -0,0 +1,54 @@ +importScripts("./Generator.mjs"); + +class GlitchWorker extends Generator { + constructor() { + super(); + + // Delay between these values + this.config = { + glitch: { min: 500, max: 2500 }, + randBg: { min: 5000, max: 5000 } + } + + this._timers = {}; + + self.addEventListener("message", event => this.message(event)); + self.postMessage("READY"); + } + + // Run a scoped function on a random interval between + queue(func) { + clearTimeout(this._timers[func]); + const next = Generator.randInt(this.config[func].min, this.config[func].max); + this._timers[func] = setTimeout(() => this.queue(func), next); + + this[func]?.(); + } + + // Set background by id and stop randBg animation + async forceBg(id) { + clearTimeout(this._timers.randBg); + + const image = await this.fetchBg(id); + this.bg.current = image; + + this.setBg(image); + } + + // Event handler for messages from parent thread + message(event) { + const data = typeof event.data === "object" ? event.data : [event.data]; + + switch(data[0]) { + case "START": + this.bg.dir = data[1]; + this.randBg(); + for(const func of Object.keys(this.config)) { + this.queue(func); + } + break; + } + } +} + +self.glitch = new GlitchWorker(); \ No newline at end of file diff --git a/assets/js/pages/about.js b/assets/js/pages/about.js new file mode 100755 index 0000000..10fe584 --- /dev/null +++ b/assets/js/pages/about.js @@ -0,0 +1,65 @@ +new vv.Interactions("about"); + +const randomIntFromInterval = (min, max) => { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +// Interest explosion effect from origin position +const explodeInterests = (originX, originY) => { + // Elements can not translate more than negative- and positive from this number + const TRANS_LIMIT = 300; + + const wrapper = document.querySelector("div.interests"); + wrapper.classList.add("active"); + + [...wrapper.querySelectorAll("p")].forEach(element => { + /* + Generate random visuals for current element + */ + const hue = randomIntFromInterval(0, 360); + const rotate = randomIntFromInterval(-5, 5); + const transX = randomIntFromInterval(TRANS_LIMIT * -1, TRANS_LIMIT); + const transY = randomIntFromInterval(TRANS_LIMIT * -1, TRANS_LIMIT); + + // Set initial position + element.style.setProperty("top", `${originY}px`); + element.style.setProperty("left", `${originX}px`); + + // Set random HUE rotation + element.style.setProperty("-webkit-filter", `hue-rotate(${hue}deg)`); + element.style.setProperty("filter", `hue-rotate(${hue}deg)`); + + // Translate and rotate to random position from origin + element.style.setProperty("transform", `translate(${transX}px, ${transY}px) rotate(${rotate}deg)`); + }); +}; + +// Interest implotion effect from explodeInterests() +const implodeInterests = () => { + const wrapper = document.querySelector("div.interests"); + wrapper.classList.remove("active"); + + [...wrapper.querySelectorAll("p")].forEach(element => { + // Reset to initial position + element.style.setProperty("transform", "translate(0, 0)"); + }); +}; + +// Bind triggers for interests explosion and implotion +{ + const interestsElement = document.querySelector("section.about span.interests"); + // 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(); + + const x = size.x - 80; + const y = size.y - 10; + + explodeInterests(x, y); + }); + + interestsElement.addEventListener(canHover ? "mouseleave" : "touchend", () => implodeInterests()); +} diff --git a/assets/js/pages/contact.js b/assets/js/pages/contact.js new file mode 100755 index 0000000..b98e377 --- /dev/null +++ b/assets/js/pages/contact.js @@ -0,0 +1,87 @@ +class ContactForm { + static STORAGE_KEY = "contact_form_message"; + + constructor(form) { + this.form = form; + + this.getSavedMessageAndPopulateFields(); + + // Save message each time a button is pressed on a form element + [...document.querySelectorAll("form :is(input, textarea)")].forEach(element => { + element.addEventListener("keyup", () => this.saveMessage()); + }); + + + } + + // Get saved message as JSON from SessionStorage + static getSavedMessage() { + const data = window.sessionStorage.getItem(ContactForm.STORAGE_KEY); + + // Return message data as JSON + return data ? JSON.parse(data) : {}; + } + + // Remove saved message from SessionStorage if it exists + static removeSavedMessage() { + return window.sessionStorage.removeItem(ContactForm.STORAGE_KEY); + } + + // Populate from input fields with data from SessionStorage + getSavedMessageAndPopulateFields() { + const message = ContactForm.getSavedMessage(); + + // Remove message and bail out if there is no saved message or if it is already sent + if (!message) { + return ContactForm.removeSavedMessage(); + } + + for (const [name, value] of Object.entries(message)) { + this.form.querySelector(`[name="${name}"]`).value = value; + } + } + + // Save current message in SessionStorage + saveMessage() { + const message = {}; + + // Copy field name and value from FormData into object + (new FormData(this.form)).forEach((v, k) => message[k] = v); + + // Save message data to SessionStorage as JSON + window.sessionStorage.setItem(ContactForm.STORAGE_KEY, JSON.stringify(message)); + } +} + +// Initialize contact form handler +{ + const form = document.querySelector("section.form form"); + + // Create a new form handler or remove any saved message if the form element can't be found + form ? (new ContactForm(form)) : ContactForm.removeSavedMessage(); +} + +// Social links hover +{ + const socialElementHover = (target) => { + const element = target.querySelector("p"); + + target.classList.add("hovering"); + 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)`); + }); + }; + + const elements = [...document.querySelectorAll("social")]; + + elements.forEach(element => { + element.addEventListener("mouseenter", () => socialElementHover(element)); + + element.addEventListener("mouseleave", () => { + elements.forEach(element => element.classList.remove("hovering")); + }); + }); +} \ No newline at end of file diff --git a/assets/js/pages/error.js b/assets/js/pages/error.js new file mode 100755 index 0000000..3ad9a04 --- /dev/null +++ b/assets/js/pages/error.js @@ -0,0 +1,47 @@ +import { default as Glitch } from "/assets/js/modules/glitch/Glitch.mjs"; + +// Start glitch canvas +const canvas = document.querySelector("canvas"); +canvas._glitch = new Glitch(canvas); + +// Text glitching +{ + const GLITCH_MAX_OFFSET_PIXELS = 5; + const GLITCH_COUNT_MAX = 4; + const UNSET_GLITCH_TIMEOUT = 100; + + const NEXT_GLITCH_MIN = 100; + const NEXT_GLITCH_MAX = 500; + + const randomIntFromInterval = (min, max) => { + return Math.floor(Math.random() * (max - min + 1) + min) + } + + const glitchText = (target) => { + const glitch = []; + + // Generate text-shadow property values + for (let i = 0; i < randomIntFromInterval(2, GLITCH_COUNT_MAX); i++) { + // Text-shadow x offset + const x = randomIntFromInterval(GLITCH_MAX_OFFSET_PIXELS * -1, GLITCH_MAX_OFFSET_PIXELS); + + // Get red or blue color from random parity + const rgb = randomIntFromInterval(0, 1) ? "255,0,0" : "0,0,55"; + // Generate random decimal transparancy + const alpha = randomIntFromInterval(30, 50) / 100; + + glitch.push(`${x}px 0 0 rgba(${rgb}, ${alpha})`); + } + + // Glitch the text! + target.style.setProperty("text-shadow", glitch.join(",")); + + // Remove glitch effect from text + setTimeout(() => target.style.setProperty("text-shadow", "unset"), UNSET_GLITCH_TIMEOUT); + + // Glitch the text again after this timeout + setTimeout(() => glitchText(target), randomIntFromInterval(NEXT_GLITCH_MIN, NEXT_GLITCH_MAX)); + }; + + [...document.querySelectorAll("[glitch-text]")].forEach(element => glitchText(element)); +} \ No newline at end of file diff --git a/assets/js/pages/index.js b/assets/js/pages/index.js new file mode 100755 index 0000000..f3219e8 --- /dev/null +++ b/assets/js/pages/index.js @@ -0,0 +1,120 @@ +const EMAIL_CPY_ANIM_DUR_MSECONDS = 1000; + +// Run email copied splash animation +const emailCopiedAnimation = () => { + const CONFETTI_COUNT = 40; + const CONFETTI_SCALE_PIXELS = 300; + + const randomIntFromInterval = (min, max) => { + return Math.floor(Math.random() * (max - min + 1) + min) + } + + // Create new splash element + const splashElement = document.createElement("splash"); + splashElement.innerText = "copied!"; + + // Set inline display to none to hide this element on pages where the splash element has no override styles defined. + splashElement.style.display = "none"; + + // Array of box-shadow strings as "confetti" + const confetti = []; + + // Generate random confetti + for (let i = 0; i < CONFETTI_COUNT; i++) { + // Random confetti position + const x = randomIntFromInterval(CONFETTI_SCALE_PIXELS * -1, CONFETTI_SCALE_PIXELS); + const y = randomIntFromInterval(CONFETTI_SCALE_PIXELS * -1, CONFETTI_SCALE_PIXELS); + + // Random confetti RGB color + const rgb = [ + randomIntFromInterval(0, 255), + randomIntFromInterval(0, 255), + randomIntFromInterval(0, 255) + ]; + + // Interpolate random values and append to outer confetti array + confetti.push(`${x}px ${y}px 0 rgb(${rgb.join(",")})`); + } + + // Set CSS variable on splash element that in turn will be used by pseudo-element + splashElement.style.setProperty("--confetti", confetti.join(",")); + + // Start animation by appending the created element to the document body + document.body.appendChild(splashElement); + + // Run hide animation + setTimeout(() => { + splashElement.classList.add("hide"); + + // Selfdestruct element when hide animation finishes + setTimeout(() => splashElement.remove(), 400); + }, EMAIL_CPY_ANIM_DUR_MSECONDS + 100); +} + +new vv.Interactions("index", { + // Copy email address to clipboard + copyEmail: async () => { + try { + await navigator.clipboard.writeText("victor@vlw.se"); + + // Run "email copied" animation! + emailCopiedAnimation(); + + // NOTE: I don't know, spamming the button is kinda fun + // Prevent interactions with the copy email elements while the animation is running + /*[...document.querySelectorAll("[vv-call='copyEmail']")].forEach(element => { + //element.classList.add("lock"); + + setTimeout(() => element.classList.remove("lock"), EMAIL_CPY_ANIM_DUR_MSECONDS); + });*/ + } catch (error) { + console.error(error.message); + } + }, + // Open the fullscreen menu + openMenu: () => document.querySelector("menu").classList.add("active"), + // Close the fullscreen menu + closeMenu: () => document.querySelector("menu").classList.remove("active") +}); + +// Change site accent color on hover of menu items +if (window.matchMedia("(hover: hover)")) { + // Update root CSS variables + const updateColor = (rgb = null, hue = 0) => { + if (!rgb) { + document.documentElement.style.removeProperty("--hue-accent"); + document.documentElement.style.removeProperty("--primer-color-accent"); + document.documentElement.style.removeProperty("--color-accent"); + + return; + } + + document.documentElement.style.setProperty("--hue-accent", `${hue}deg`); + + document.documentElement.style.setProperty("--primer-color-accent", `${rgb}`); + // Compiled color variable must to be updated to receive the new RGB values + document.documentElement.style.setProperty("--color-accent", "rgb(var(--primer-color-accent)"); + }; + + [...document.querySelectorAll("menu li")].forEach(element => { + // Change site accent color to RGB and HUE rotation defined in element dataset + element.addEventListener("mouseenter", (event) => updateColor(event.target.dataset.rgb, event.target.dataset.hue)); + // Reset initial accent color and hues + element.addEventListener("mouseleave", () => updateColor()); + }); + + // Reset color on navigation + document.querySelector(vv._env.MAIN).addEventListener(vv.Navigation.events.LOADED, () => updateColor(), { once: true }); +} + +// Open search box from mobile fullscreen menu +{ + // Open search dialog when searchbox is clicked + document.querySelector("menu searchbox").addEventListener("click", () => { + // Search box dialog element + document.querySelector("dialog.search").showModal(); + + // Close fullscreen menu + document.querySelector("menu").classList.remove("active"); + }); +} \ No newline at end of file diff --git a/assets/js/pages/search.js b/assets/js/pages/search.js new file mode 100755 index 0000000..6964dbc --- /dev/null +++ b/assets/js/pages/search.js @@ -0,0 +1,25 @@ +// Don't open the search dialog overlay if search page is open stand-alone +{ + const searchBox = document.querySelector("body:not(.search-dialog-open) searchbox"); + + // Page is stand-alone + if (searchBox) { + // Shift focus to the on-page search box instead of opening search dialog on click + const shiftSearchboxFocus = () => { + // Override normal "open search dialog" behavior + document.querySelector("dialog.search").close(); + + // Shift focus to the on-page search input instead + } + + // Bind event listener to searchbox element + document.querySelector("body:not(.search-dialog-open) searchbox").addEventListener("click", shiftSearchboxFocus, true); + + // Remove event listener from searchbox element on page navigation + mainElement.addEventListener(vv.Navigation.events.LOADING, () => { + searchBox.removeEventListener("click", shiftSearchboxFocus); + }); + } +} + +new vv.Interactions("search"); \ No newline at end of file diff --git a/assets/js/pages/work.js b/assets/js/pages/work.js new file mode 100755 index 0000000..723281b --- /dev/null +++ b/assets/js/pages/work.js @@ -0,0 +1 @@ +new vv.Interactions("work"); \ No newline at end of file diff --git a/assets/media/gazing.jpg b/assets/media/gazing.jpg new file mode 100755 index 0000000000000000000000000000000000000000..3cf3b9b04bee421d013e663b09b080cf473f6f37 GIT binary patch literal 63085 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 hz3tj-zSjpFy)FA0?4M \ No newline at end of file diff --git a/assets/media/icons/email.svg b/assets/media/icons/email.svg new file mode 100755 index 0000000..aa552a9 --- /dev/null +++ b/assets/media/icons/email.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/media/icons/github.svg b/assets/media/icons/github.svg new file mode 100755 index 0000000..88fffd2 --- /dev/null +++ b/assets/media/icons/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/media/icons/libera.svg b/assets/media/icons/libera.svg new file mode 100755 index 0000000..53cd5b3 --- /dev/null +++ b/assets/media/icons/libera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/media/icons/mastodon.svg b/assets/media/icons/mastodon.svg new file mode 100755 index 0000000..8997de1 --- /dev/null +++ b/assets/media/icons/mastodon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/media/icons/pin.svg b/assets/media/icons/pin.svg new file mode 100755 index 0000000..e3dd53f --- /dev/null +++ b/assets/media/icons/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/media/icons/search.svg b/assets/media/icons/search.svg new file mode 100755 index 0000000..159fe9a --- /dev/null +++ b/assets/media/icons/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/media/line.svg b/assets/media/line.svg new file mode 100755 index 0000000..27842be --- /dev/null +++ b/assets/media/line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/media/vw.svg b/assets/media/vw.svg new file mode 100755 index 0000000..b7c368b --- /dev/null +++ b/assets/media/vw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100755 index 0000000..e88fad0 --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "reflect/client": "^2.1" + } +} diff --git a/composer.lock b/composer.lock new file mode 100755 index 0000000..e0e45b8 --- /dev/null +++ b/composer.lock @@ -0,0 +1,56 @@ +{ + "_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": "3e10269d358ba18734f4f011cd22c42e", + "packages": [ + { + "name": "reflect/client", + "version": "2.1.4", + "source": { + "type": "git", + "url": "https://github.com/VictorWesterlund/reflect-client-php.git", + "reference": "47cee961d1bfdd9261a58dde753d824947e91636" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/VictorWesterlund/reflect-client-php/zipball/47cee961d1bfdd9261a58dde753d824947e91636", + "reference": "47cee961d1bfdd9261a58dde753d824947e91636", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Reflect\\": "src/Reflect/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-only" + ], + "authors": [ + { + "name": "Victor Westerlund", + "email": "victor.vesterlund@gmail.com" + } + ], + "description": "Extendable PHP interface for communicating with Reflect API over HTTP or UNIX sockets", + "support": { + "issues": "https://github.com/VictorWesterlund/reflect-client-php/issues", + "source": "https://github.com/VictorWesterlund/reflect-client-php/tree/2.1.4" + }, + "time": "2023-08-18T14:41:31+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/pages/about.php b/pages/about.php new file mode 100755 index 0000000..ade45ed --- /dev/null +++ b/pages/about.php @@ -0,0 +1,73 @@ +call("/coffee", Method::GET); + + $offset = 86400; // 24 hours in seconds + $now = time(); + + // Get only timestamps from response + $coffee_dates = array_column($resp[1], "date_timestamp_created"); + // Filter array for timestamps between now and $offset + $coffee_last_day = array_filter($coffee_dates, fn(int $time): bool => $time >= ($now - $offset)); + + return count($coffee_last_day); + } + +?> + +

+ +

Victor Westerlund

+
+ +
+

I​'m a full-stack web developer from Sweden, currently working as IT-Lead at iCellate Medical in Solna, Stockholm - a biopharma start-up developing precision oncology. I develop and maintain my own web framework and use it to build web apps and websites - including this one.

+

The <programming/markup/command>-languages I currently use the most are (in a mostly accurate decending order): PHP, JavaScript, CSS, MySQL, Python, SQLite, Bash, and [raw] HTML. In the process of learning Rust!

+
+
+

This website

+

This site and all of its components are 100% Free Software; licensed under the GNU GPLv3. It's built on top of my own Vegvisir (web) and Reflect (API) framework. There are no cookies or trackers on this site and analytics only consist of basic access and error logs; and from which IP address.

+
+
+

Projects

+

These are my top projects I'm working on right now:

+

* Vegvisir: A web framework written in PHP, for PHP developers.

+

* Reflect: An API framework also written in PHP, for PHP developers.

+

See more on my works page. And even more including smaller projects on my GitHub.

+
+
+

Personal

+

At times, I can become a real sucker for a variety of topics I find interesting, and spend hours reading as much as I can about them too. When I'm not glued to a computer screen, I like me some skiing and occasional hobby photography. I'm also a real coffeeholic.

+

Let's work on something together, have a chat, or anything else. write me a line!

+
+
+

Philosophy

+

I believe in a world where humans treat other humans as humans, not products of profit and control. While my focus primarily lies in software freedom - that is, software that respects the user's right to freedom. My main goal is to preserve and promote liberalism.

+ See my unstructured "blog" for posts (rants) about this now and then if this sounds interesting to you.

?> +
+
+
+

website version:

+
+ + + \ No newline at end of file diff --git a/pages/about/version.php b/pages/about/version.php new file mode 100755 index 0000000..3586e09 --- /dev/null +++ b/pages/about/version.php @@ -0,0 +1,19 @@ + + \ No newline at end of file diff --git a/pages/contact.php b/pages/contact.php new file mode 100755 index 0000000..749ff82 --- /dev/null +++ b/pages/contact.php @@ -0,0 +1,81 @@ +call("messages", Method::POST, $_POST); + + // Set message sent to true if ok, false if something went wrong + $message_sent = $post_message[0] === 201; + } + +?> + +
+

Let's chat

+

The best way to get in touch is by email, or with the form on this page. The time in Sweden right now is so I will probably reply within a few hours.

+
+ + +
+ +

encrypt your message with my OpenPGP key.

+

my key is also listed on the openPGP key server for victor@vlw.se so your e-mail client can automatically retreive it if supported.

+ +
+ + + + + + + +
+

😟 Oh no, something went wrong

+

Response from API:

+
+
+ + +
+
+ + + + + + + + + +
+
+ +
+

🙏 Message sent!

+
+ + + \ No newline at end of file diff --git a/pages/document.php b/pages/document.php new file mode 100755 index 0000000..6cedad1 --- /dev/null +++ b/pages/document.php @@ -0,0 +1,71 @@ + + + + + + + + + + + + + Victor Westerlund + + +
+ + + +

search anything...

+
+ + + +
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/pages/error.php b/pages/error.php new file mode 100755 index 0000000..623b01a --- /dev/null +++ b/pages/error.php @@ -0,0 +1,6 @@ + + +
+

404

+
+ \ No newline at end of file diff --git a/pages/index.php b/pages/index.php new file mode 100755 index 0000000..7dc636a --- /dev/null +++ b/pages/index.php @@ -0,0 +1,31 @@ + + + + + + + \ No newline at end of file diff --git a/pages/search.php b/pages/search.php new file mode 100755 index 0000000..91910b0 --- /dev/null +++ b/pages/search.php @@ -0,0 +1,107 @@ +call("/search?q={$query}", Method::GET) : null; + + // ISO 8601: YYYY-MM-DD + $date_format = "Y-m-d"; + +?> + + + + + + + + + + + + 0): ?> + + + + + +
+

Work

+

search result(s) from my public work

+
+
+ + " vv="search" vv-call="navigate">
+

+

+

+
+ +
+ + + + +
+

No results for search term ""

+
+ + + + + + + + + + $error_msg): ?> + + + +
+

Unknown request validation error

+
+ + + + +
+

Type at least characters to search!

+
+ + + + + + + + +
+

Something went wrong

+
+ + + + + + + + + \ No newline at end of file diff --git a/pages/work.php b/pages/work.php new file mode 100755 index 0000000..0636062 --- /dev/null +++ b/pages/work.php @@ -0,0 +1,162 @@ +call("/work", Method::GET); + +?> + + +
+ +

Most of my free open-source software is available on GitHub and it's also mirrored on my server

+ +
+ + + [[02 => [14 => []]]]] + */ + + $rows = []; + // Create array of arrays ordered by decending year, month, day, items + foreach ($resp[1] as $row) { + // Create array for current year if it doesn't exist + if (!array_key_exists($row["date_year"], $rows)) { + $rows[$row["date_year"]] = []; + } + + // Create array for current month if it doesn't exist + if (!array_key_exists($row["date_month"], $rows[$row["date_year"]])) { + $rows[$row["date_year"]][$row["date_month"]] = []; + } + + // Create array for current day if it doesn't exist + if (!array_key_exists($row["date_day"], $rows[$row["date_year"]][$row["date_month"]])) { + $rows[$row["date_year"]][$row["date_month"]][$row["date_day"]] = []; + } + + // Append item to ordered array + $rows[$row["date_year"]][$row["date_month"]][$row["date_day"]][] = $row; + } + + ?> + +
+ + $months): ?> +
+
+

+
+ +
+ + $days): ?> +
+
+ +

+
+ +
+ + $items): ?> +
+
+ +

+
+ +
+ +
+ + + +
+ +

">

+ +
+ + + + +

+ + + + + + + + + + + + ." type=""> + + + + ." type="" loading="lazy"/> + + + +

+ + + +
+ + + + > + +
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+

This is not really the end of the list. I will add some of my notable older work at some point.

+
+ +

Something went wrong!

+ + + \ No newline at end of file