mirror of
https://codeberg.org/vlw/vlw.se.git
synced 2025-09-13 21:13:40 +02:00
Compare commits
2 commits
6bbb6926da
...
b0ab6a4f5b
Author | SHA1 | Date | |
---|---|---|---|
b0ab6a4f5b | |||
7ee2ba631d |
13 changed files with 327 additions and 22 deletions
|
@ -5,4 +5,5 @@ pass = ""
|
|||
|
||||
[databases]
|
||||
vlw = ""
|
||||
playground = ""
|
||||
battlestation = ""
|
40
api/endpoints/playground/coffee/DELETE.php
Normal file
40
api/endpoints/playground/coffee/DELETE.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Reflect\Path;
|
||||
use Reflect\Response;
|
||||
use ReflectRules\Type;
|
||||
use ReflectRules\Rules;
|
||||
use ReflectRules\Ruleset;
|
||||
|
||||
use const VLW\API\RESP_DELETE_OK;
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
use VLW\API\Databases\VLWdb\Models\Playground\Coffee\CoffeeModel;
|
||||
|
||||
require_once Path::root("src/databases/VLWdb.php");
|
||||
require_once Path::root("src/databases/models/Playground/Coffee/Coffee.php");
|
||||
|
||||
class DELETE_PlaygroundCoffee extends VLWdb {
|
||||
protected Ruleset $ruleset;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->ruleset = new Ruleset(strict: true);
|
||||
|
||||
$this->ruleset->GET([
|
||||
(new Rules(CoffeeModel::ID->value))
|
||||
->required()
|
||||
->type(Type::NUMBER)
|
||||
]);
|
||||
|
||||
parent::__construct(Databases::PLAYGROUND, $this->ruleset);
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
return $this->db->for(WorkActionsModel::TABLE)->delete($_POST) === true
|
||||
? new Response(RESP_DELETE_OK)
|
||||
: new Response("D:", 500);
|
||||
}
|
||||
}
|
40
api/endpoints/playground/coffee/GET.php
Normal file
40
api/endpoints/playground/coffee/GET.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Reflect\Path;
|
||||
use Reflect\Response;
|
||||
use ReflectRules\Type;
|
||||
use ReflectRules\Rules;
|
||||
use ReflectRules\Ruleset;
|
||||
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
use VLW\API\Databases\VLWdb\Models\Playground\Coffee\CoffeeModel;
|
||||
|
||||
require_once Path::root("src/databases/VLWdb.php");
|
||||
require_once Path::root("src/databases/models/Playground/Coffee/Coffee.php");
|
||||
|
||||
class GET_PlaygroundCoffee extends VLWdb {
|
||||
protected Ruleset $ruleset;
|
||||
|
||||
public function __construct() {
|
||||
$this->ruleset = new Ruleset(strict: true);
|
||||
|
||||
parent::__construct(Databases::PLAYGROUND, $this->ruleset);
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
$result = $this->db
|
||||
->for(CoffeeModel::TABLE)
|
||||
->order([CoffeeModel::ID->value => "DESC"])
|
||||
->select([
|
||||
CoffeeModel::ID->value
|
||||
]);
|
||||
|
||||
return $result->num_rows > 0
|
||||
// Return entries as sequential array of integer timestamps
|
||||
? new Response(array_map(fn(array $value): int => $value[0], $result->fetch_all(MYSQLI_NUM)))
|
||||
: new Response([], 404);
|
||||
}
|
||||
}
|
38
api/endpoints/playground/coffee/POST.php
Normal file
38
api/endpoints/playground/coffee/POST.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Reflect\Path;
|
||||
use Reflect\Response;
|
||||
use ReflectRules\Type;
|
||||
use ReflectRules\Rules;
|
||||
use ReflectRules\Ruleset;
|
||||
|
||||
use VLW\API\Databases\VLWdb\{
|
||||
VLWdb,
|
||||
Databases
|
||||
};
|
||||
use VLW\API\Databases\VLWdb\Models\Playground\Coffee\CoffeeModel;
|
||||
|
||||
require_once Path::root("src/databases/VLWdb.php");
|
||||
require_once Path::root("src/databases/models/Playground/Coffee/Coffee.php");
|
||||
|
||||
class POST_PlaygroundCoffee extends VLWdb {
|
||||
protected Ruleset $ruleset;
|
||||
|
||||
public function __construct() {
|
||||
$this->ruleset = new Ruleset(strict: true);
|
||||
|
||||
$this->ruleset->GET([
|
||||
(new Rules(CoffeeModel::ID->value))
|
||||
->type(Type::NUMBER)
|
||||
->default(time())
|
||||
]);
|
||||
|
||||
parent::__construct(Databases::PLAYGROUND, $this->ruleset);
|
||||
}
|
||||
|
||||
public function main(): Response {
|
||||
return $this->db->for(CoffeeModel::TABLE)->insert($_POST) === true
|
||||
? new Response($_POST[CoffeeModel::ID->value], 201)
|
||||
: new Response(":(", 500);
|
||||
}
|
||||
}
|
|
@ -15,6 +15,8 @@
|
|||
case WORK_TAGS = "/work/tags";
|
||||
case WORK_ACTIONS = "/work/actions";
|
||||
|
||||
case PLAYGROUND_COFFEE = "/playground/coffee";
|
||||
|
||||
case BATTLESTATION = "/battlestation";
|
||||
case BATTLESTATION_MB = "/battlestation/mb";
|
||||
case BATTLESTATION_CPU = "/battlestation/cpu";
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
enum Databases: string {
|
||||
case VLW = "vlw";
|
||||
case PLAYGROUND = "playground";
|
||||
case BATTLESTATION = "battlestation";
|
||||
}
|
||||
|
||||
|
|
9
api/src/databases/models/Playground/Coffee/Coffee.php
Normal file
9
api/src/databases/models/Playground/Coffee/Coffee.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace VLW\API\Databases\VLWdb\Models\Playground\Coffee;
|
||||
|
||||
enum CoffeeModel: string {
|
||||
const TABLE = "coffee";
|
||||
|
||||
case ID = "id";
|
||||
}
|
|
@ -20,6 +20,8 @@ header {
|
|||
}
|
||||
|
||||
img {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
|
@ -28,20 +30,20 @@ img {
|
|||
/* ## Title */
|
||||
|
||||
section.title {
|
||||
gap: var(--padding);
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
min-height: 30vh;
|
||||
min-height: 50vw;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
padding-bottom: var(--padding);
|
||||
}
|
||||
|
||||
section.title img {
|
||||
--outline-width: 5px;
|
||||
--outline-color: black;
|
||||
|
||||
width: 100%;
|
||||
transform: translateY(-10px);
|
||||
width: 10vw;
|
||||
transform: translateY(-1vw);
|
||||
animation: bouncy 1s infinite alternate ease-in-out;
|
||||
filter:
|
||||
drop-shadow(0 10px 0 blue)
|
||||
|
@ -53,17 +55,47 @@ section.title img {
|
|||
}
|
||||
|
||||
@keyframes bouncy {
|
||||
25% { transform: translateY(10px); }
|
||||
100% { transform: translateY(10px); }
|
||||
25% { transform: translateY(0); }
|
||||
100% { transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ### Super Secret Settings */
|
||||
|
||||
section.sss {
|
||||
border-radius: 6px;
|
||||
padding: var(--padding);
|
||||
margin: var(--padding) 0;
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
section.sss button {
|
||||
margin-top: var(--padding);
|
||||
}
|
||||
|
||||
/* ## Coffee */
|
||||
|
||||
section.coffee {
|
||||
display: grid;
|
||||
gap: var(--padding);
|
||||
grid-template-columns: 100px 1fr;
|
||||
}
|
||||
|
||||
section.coffee img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* # Size queries */
|
||||
|
||||
@media (min-width: 700px) {
|
||||
section.title {
|
||||
gap: var(--padding);
|
||||
margin: 50px 0;
|
||||
min-height: unset;
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
}
|
||||
|
||||
section.title img {
|
||||
width: 100%;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
/* # Sections */
|
||||
|
||||
super-secret-settings {
|
||||
--border-width: 5px;
|
||||
|
||||
position: fixed;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
top: var(--border-width);
|
||||
left: var(--border-width);
|
||||
width: 400px;
|
||||
fill: white;
|
||||
color: white;
|
||||
|
@ -11,8 +13,8 @@ super-secret-settings {
|
|||
border: solid 1px white;
|
||||
background-color: rgb(0, 102, 204);
|
||||
box-shadow:
|
||||
0 0 0 5px black,
|
||||
0 0 40px 10px rgba(0, 0, 0, .4);
|
||||
0 0 0 var(--border-width) black,
|
||||
0 0 40px calc(var(--border-width) * 2) rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
/* ## Header */
|
||||
|
@ -20,12 +22,15 @@ super-secret-settings {
|
|||
super-secret-settings section.header {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
border-bottom: solid 1px white;
|
||||
grid-template-columns: 1fr repeat(2, var(--running-size));
|
||||
height: var(--running-size);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
super-secret-settings:not(.collapsed) section.header {
|
||||
border-bottom: solid 1px white;
|
||||
}
|
||||
|
||||
super-secret-settings section.header:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
@ -42,7 +47,11 @@ super-secret-settings section.header svg {
|
|||
|
||||
super-secret-settings section.header button {
|
||||
height: 100%;
|
||||
border-left: solid 1px var(--border-color);
|
||||
border-left: solid 1px white;
|
||||
}
|
||||
|
||||
super-secret-settings.collapsed section.header button.collapse svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* ## Body */
|
||||
|
@ -50,3 +59,18 @@ super-secret-settings section.header button {
|
|||
super-secret-settings section.body {
|
||||
padding: var(--padding);
|
||||
}
|
||||
|
||||
super-secret-settings.collapsed section.body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* # Size queries */
|
||||
|
||||
@media not (hover: hover), (max-width: 900px) {
|
||||
super-secret-settings {
|
||||
top: unset;
|
||||
bottom: 5px;
|
||||
width: calc(100% - (var(--border-width) * 2));
|
||||
transform: translate(0, 0) !important;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
|
||||
|
||||
new Elevent("click", document.querySelector("section.sss button"), () => {
|
||||
new Elevent("click", document.querySelector("section.sss button"), (event) => {
|
||||
const SSS_TAG_NAME = "super-secret-settings";
|
||||
|
||||
let sssElement = document.querySelector(SSS_TAG_NAME);
|
||||
|
@ -11,6 +11,13 @@ new Elevent("click", document.querySelector("section.sss button"), () => {
|
|||
document.body.appendChild(sssElement);
|
||||
}
|
||||
|
||||
const size = event.target.getBoundingClientRect();
|
||||
|
||||
// Set initial window position above button
|
||||
const y = size.top;
|
||||
const x = size.left + size.width;
|
||||
sssElement.style.transform = `translate(${x}px, ${y}px)`
|
||||
|
||||
// Navigate SSS element to playground page
|
||||
new vegvisir.Navigation("/playground/super-secret-settings").navigate(sssElement);
|
||||
});
|
|
@ -8,6 +8,7 @@ const clampRight = (window.innerWidth - sssElement._size.width) - (borderWidth *
|
|||
const clampBottom = (window.innerHeight - sssElement._size.height) - (borderWidth * 3);
|
||||
|
||||
const moveWindow = new Elevent("mousemove", null, (event) => {
|
||||
// Clamp window to viewport
|
||||
const y = Math.max(borderWidth, Math.min(clampBottom, (event.clientY - 40)));
|
||||
const x = Math.max(borderWidth, Math.min(clampRight, event.clientX - (sssElement._size.width / 2)));
|
||||
|
||||
|
@ -34,4 +35,5 @@ new Elevent("mousedown", sssElement.querySelector(".header"), (event) => {
|
|||
});
|
||||
});
|
||||
|
||||
new Elevent("click", sssElement.querySelector(".header button"), () => sssElement.remove());
|
||||
new Elevent("click", sssElement.querySelector(".header button.close"), () => sssElement.remove());
|
||||
new Elevent("click", sssElement.querySelector(".header button.collapse"), () => sssElement.classList.toggle("collapsed"));
|
|
@ -1,20 +1,129 @@
|
|||
<?php
|
||||
|
||||
use Reflect\Response;
|
||||
|
||||
use VLW\Client\API;
|
||||
use VLW\API\Endpoints;
|
||||
|
||||
use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
|
||||
|
||||
require_once VV::root("src/client/API.php");
|
||||
require_once VV::root("api/src/Endpoints.php");
|
||||
|
||||
function to_plural(int $value): string {
|
||||
return $value !== 1 ? "s" : "";
|
||||
}
|
||||
|
||||
$time = new class extends DateTimeImmutable {
|
||||
public function __construct() {
|
||||
parent::__construct("now", new DateTimeZone($_ENV["time"]["date_time_zone"]));
|
||||
}
|
||||
|
||||
public function hours(): int {
|
||||
return (int) $this->format("G");
|
||||
}
|
||||
|
||||
public function minutes(): int {
|
||||
return (int) $this->format("i");
|
||||
}
|
||||
|
||||
public function since_midnight_str(): string {
|
||||
$hours = $this->hours();
|
||||
$minutes = $this->minutes();
|
||||
|
||||
// Show minutes if we're not into the first hour yet
|
||||
if ($hours < 1) {
|
||||
return $minutes . " minute" . to_plural($minutes);
|
||||
}
|
||||
|
||||
// Round up to the nearest hour
|
||||
return ($minutes < 45 ? $hours : $hours + 1) . " hour" . to_plural($hours);
|
||||
}
|
||||
};
|
||||
|
||||
$coffee = new class extends API {
|
||||
const THRESHOLDS = [
|
||||
[-10, "a lot less than"],
|
||||
[-5, "a bit less than"],
|
||||
[0, "average"],
|
||||
[5, "a bit more than"],
|
||||
[10, "a lot more than"]
|
||||
];
|
||||
|
||||
private readonly int $now;
|
||||
private readonly Response $resp;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
|
||||
$this->now = time();
|
||||
$this->resp = $this->call(Endpoints::PLAYGROUND_COFFEE->value)->get();
|
||||
}
|
||||
|
||||
private function get_until_timestamp(int $offset = 0): array {
|
||||
return array_filter($this->resp->json(), fn(int $timestamp): int => $timestamp > $offset);
|
||||
}
|
||||
|
||||
private function get_average_month(): int {
|
||||
$days = [0];
|
||||
|
||||
foreach ($this->get_until_timestamp(2592000) as $timestamp) {
|
||||
// Append new value for next 24 hours
|
||||
if ($timestamp % 86400 === 0) {
|
||||
$days[] = 0;
|
||||
}
|
||||
|
||||
$days[count($days) - 1]++;
|
||||
}
|
||||
|
||||
// Count the average for each day
|
||||
return array_sum($days) / count($days);
|
||||
}
|
||||
|
||||
// Return the number of cups within the last 24 hours
|
||||
public function get_today(): int {
|
||||
return $this->resp->ok ? count($this->get_until_timestamp(86400)) : 0;
|
||||
}
|
||||
|
||||
public function get_average_month_str(): string {
|
||||
$diff = $this->get_today() - $this->get_average_month();
|
||||
|
||||
// Check diff of today against monthyl average thresholds
|
||||
foreach (self::THRESHOLDS as $threshold) {
|
||||
[$limit, $msg] = $threshold;
|
||||
|
||||
// Bail out if diff is less than threshold limit
|
||||
if ($diff < $limit) {
|
||||
return $msg;
|
||||
}
|
||||
}
|
||||
|
||||
// Return last entry if diff is more than last threshold limit
|
||||
return self::THRESHOLDS[count(self::THRESHOLDS) - 1][1];
|
||||
}
|
||||
};
|
||||
|
||||
?>
|
||||
<style><?= VV::css("public/assets/css/pages/playground/index") ?></style>
|
||||
<section class="title">
|
||||
|
||||
<?php // I was listening to https://www.youtube.com/watch?v=DsUb4Lq6DBE when I made the dancing letters ?>
|
||||
<!-- I was listening to https://www.youtube.com/watch?v=DsUb4Lq6DBE when I made the dancing letters -->
|
||||
<?php foreach (str_split("playground") as $idx => $char): ?>
|
||||
<img src="data:image/gif;base64,<?= base64_encode(VV::embed("public/assets/media/playground/{$char}.gif")) ?>" style="animation-delay:<?= ($idx % rand(2, 4)) * 500 ?>ms">
|
||||
<?php endforeach; ?>
|
||||
|
||||
</section>
|
||||
<section class="sss">
|
||||
<button class="inline">Super Secret Settings</button>
|
||||
<h1>I'm so bored.</h1>
|
||||
<p>Don't like the way my website looks? These oficially very Super Secret Settings<sup><a href="https://minecraft-archive.fandom.com/wiki/Super_Secret_Settings">™</a></sup> will give you something else to look at!</p>
|
||||
<button class="inline">Try it</button>
|
||||
</section>
|
||||
<section class="coffee">
|
||||
<img src="data:image/gif;base64,<?= base64_encode(VV::embed("public/assets/media/playground/coffee.gif")) ?>">
|
||||
<div>
|
||||
<h1>How much coffee is enough coffee?</h1>
|
||||
<p>Woah, I 've had 4 cups of coffee today (counting from and to midnight). That's about the average amount for me (from the last 30 days)</p>
|
||||
<h1>What else.. I've had <?= $coffee->get_today() ?> cup<?= to_plural($coffee->get_today()) ?> of coffee today!</h1>
|
||||
<p>That's <?= $coffee->get_average_month_str() ?> the daily average for me (last 30 days). Here I'm counting how many ~300ml cups of coffee I've had since midnight for my timezone <?= $_ENV["time"]["date_time_zone"] ?>, that was <?= $time->since_midnight_str() ?> ago.</p>
|
||||
<p>How much coffee is enough coffee? <a href="<?= $_ENV["api"]["base_url"] . Endpoints::PLAYGROUND_COFFEE->value ?>">Click here to see my answer!!</a> (NOT CLICKBAIT). Each array value is a UNIX timestamp that represents a single cup of coffee.</p>
|
||||
</div>
|
||||
</section>
|
||||
<script type="module"><?= VV::js("public/assets/js/pages/playground/index") ?></script>
|
|
@ -1,8 +1,8 @@
|
|||
<style><?= VV::css("public/assets/css/pages/playground/super-secret-settings") ?></style>
|
||||
<section class="header">
|
||||
<p>Super Secret Settings</p>
|
||||
<button><?= VV::embed("public/assets/media/icons/chevron.svg") ?></button>
|
||||
<button><?= VV::embed("public/assets/media/icons/close.svg") ?></button>
|
||||
<button class="collapse"><?= VV::embed("public/assets/media/icons/chevron.svg") ?></button>
|
||||
<button class="close"><?= VV::embed("public/assets/media/icons/close.svg") ?></button>
|
||||
</section>
|
||||
<section class="body">
|
||||
<p>Do you have what it takes to click this button?</p>
|
||||
|
|
Loading…
Add table
Reference in a new issue