wip: 2025-07-31T07:36:19+0200 (1753940179)

This commit is contained in:
Victor Westerlund 2025-07-31 07:36:19 +02:00
parent eb2c7b7d82
commit c27df3d946
Signed by: vlw
GPG key ID: D0AD730E1057DFC6
17 changed files with 249 additions and 190 deletions

View file

@ -1,26 +1,23 @@
<?php <?php
use VLW\Database\Models\Coffee\Stats; use VLW\Database\Models\Coffee\Coffee;
use VLW\Database\Models\About\Language; use VLW\Database\Models\Languages\Language;
use const VLW\{
FORGEJO_HREF,
FORGEJO_SI_BYTE_MULTIPLE,
DEFAULT_BUTTON_ICON
};
require_once VV::root("src/Consts.php"); require_once VV::root("src/Database/Models/Coffee/Coffee.php");
require_once VV::root("src/Database/Models/Coffee/Stats.php"); require_once VV::root("src/Database/Models/Languages/Language.php");
require_once VV::root("src/Database/Models/About/Language.php");
const FORGEJO = "https://git.vlw.se/explore/repos?language=";
const SI_BYTE_MULTIPLE = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
$languages = new class extends Language { $languages = new class extends Language {
private readonly int $total_bytes; private readonly int $total_bytes;
public function __construct() { public function __construct() {
$this->total_bytes = array_sum(array_map(fn(Language $language): int => $language->bytes(), parent::all())); $this->total_bytes = array_sum(array_map(fn(Language $language): int => $language->bytes, parent::all()));
} }
public function percent(Language $language, int $mode = PHP_ROUND_HALF_UP): int { public function percent(Language $language, int $mode = PHP_ROUND_HALF_UP): int {
return round(($language->bytes() / $this->total_bytes) * 100, 0, $mode); return round(($language->bytes / $this->total_bytes) * 100, 0, $mode);
} }
public function percent_string(Language $language): string { public function percent_string(Language $language): string {
@ -29,17 +26,25 @@
public function bytes_si_string(Language $language): string { public function bytes_si_string(Language $language): string {
// Calculate factor for unit // Calculate factor for unit
$factor = floor((strlen($language->bytes()) - 1) / 3); $factor = floor((strlen($language->bytes) - 1) / 3);
// Divide by radix 10 // Divide by radix 10
$format = $language->bytes() / pow(1000, $factor); $format = $language->bytes / pow(1000, $factor);
return round($format) . " " . FORGEJO_SI_BYTE_MULTIPLE[$factor]; return round($format) . " " . SI_BYTE_MULTIPLE[$factor];
} }
}; };
$coffee = new class extends Stats { $coffee = new class extends Coffee {
public readonly int $count_week;
public readonly int $count_week_average;
public function __construct() {
$this->count_week = parent::count_week();
$this->count_week_average = parent::count_week_average();
}
public function week_average_string(): string { public function week_average_string(): string {
$diff = $this->week() - $this->week_average(); $diff = $this->count_week - $this->count_week_average;
return match (true) { return match (true) {
$diff < 0 => "less than", $diff < 0 => "less than",
@ -65,8 +70,8 @@
<stacked-bar-chart> <stacked-bar-chart>
<?php foreach ($languages::all() as $language): ?> <?php foreach ($languages::all() as $language): ?>
<a href="<?= FORGEJO_HREF . $language->id ?>" target="_blank"><chart-segment style="--size:<?= $languages->percent($language) ?>%;" data-lang="<?= $language->id ?>" data-bytes="<?= $language->bytes() ?>"> <a href="<?= FORGEJO . $language->name ?>" target="_blank"><chart-segment style="--size:<?= $languages->percent($language) ?>%;" data-lang="<?= $language->name ?>" data-bytes="<?= $language->bytes ?>">
<span data-hover><strong><?= $languages->percent_string($language) ?> <?= $language->id ?></strong><br>(<?= $language->bytes() ?> bytes)</span> <span data-hover><strong><?= $languages->percent_string($language) ?> <?= $language->name ?></strong><br>(<?= $language->bytes ?> bytes)</span>
</chart-segment></a> </chart-segment></a>
<?php endforeach; ?> <?php endforeach; ?>
@ -74,11 +79,11 @@
<languages-list> <languages-list>
<?php foreach ($languages::all() as $language): ?> <?php foreach ($languages::all() as $language): ?>
<a href="<?= FORGEJO_HREF . $language->id ?>"><button data-lang="<?= $language->id ?>" class="inline"> <a href="<?= FORGEJO . $language->name ?>"><button data-lang="<?= $language->name ?>" class="inline">
<p><?= $languages->percent_string($language) ?></p> <p><?= $languages->percent_string($language) ?></p>
<p class="lang"><?= $language->id ?></p> <p class="lang"><?= $language->name ?></p>
<p><?= $languages->bytes_si_string($language) ?></p> <p><?= $languages->bytes_si_string($language) ?></p>
<?= VV::embed(DEFAULT_BUTTON_ICON) ?> <?= VV::embed("public/assets/media/icons/chevron.svg") ?>
</button></a> </button></a>
<?php endforeach; ?> <?php endforeach; ?>
@ -86,8 +91,8 @@
<stacked-bar-chart> <stacked-bar-chart>
<?php foreach ($languages::all() as $language): ?> <?php foreach ($languages::all() as $language): ?>
<a href="<?= FORGEJO_HREF . $language->id ?>" target="_blank"><chart-segment style="--size:<?= $languages->percent($language) ?>%;" data-lang="<?= $language->id ?>" data-bytes="<?= $language->bytes() ?>"> <a href="<?= FORGEJO . $language->name ?>" target="_blank"><chart-segment style="--size:<?= $languages->percent($language) ?>%;" data-lang="<?= $language->name ?>" data-bytes="<?= $language->bytes ?>">
<span data-hover><strong><?= $languages->percent_string($language) ?> <?= $language->id ?></strong><br>(<?= $language->bytes() ?> bytes)</span> <span data-hover><strong><?= $languages->percent_string($language) ?> <?= $language->name ?></strong><br>(<?= $language->bytes ?> bytes)</span>
</chart-segment></a> </chart-segment></a>
<?php endforeach; ?> <?php endforeach; ?>
@ -101,7 +106,7 @@
</section> </section>
<section class="about"> <section class="about">
<h2>Personal</h2> <h2>Personal</h2>
<p>One thing that most people know about me is that I like coffee.. lots of coffee. In fact, I've had <?= $coffee->week() ?> cup<?= $coffee->week() === 1 ? "" : "s" ?> of coffee in the last 7 days! That's <?= $coffee->week_average_string() ?> my average of <?= $coffee->week_average() ?> per week, impressive! Even though you just read that.. I don't consider myself <i>too much</i> of a coffee snob! As long as it's dark roast and warm, I'm probably happy to have it.</p> <p>One thing that most people know about me is that I like coffee.. lots of coffee. In fact, I've had <?= $coffee->count_week ?> cup<?= $coffee->count_week === 1 ? "" : "s" ?> of coffee in the last 7 days! That's <?= $coffee->week_average_string() ?> my average of <?= $coffee->count_week_average ?> per week, impressive! Even though you just read that.. I don't consider myself <i>too much</i> of a coffee snob! As long as it's dark roast and warm, I'm probably happy to have it.</p>
<p>At times, I become a true, amateur, armchair detective for a <span class="interests">variety of your typical-nerdy topics that I find interesting</span> and you can bet I spend way more time reading about those things than I will ever have use for in life.</p> <p>At times, I become a true, amateur, armchair detective for a <span class="interests">variety of your typical-nerdy topics that I find interesting</span> and you can bet I spend way more time reading about those things than I will ever have use for in life.</p>
<p>Another silent passion of mine that comes out every few years is building computers and fiddling with networking stuff.</p> <p>Another silent passion of mine that comes out every few years is building computers and fiddling with networking stuff.</p>
</section> </section>
@ -132,4 +137,4 @@
<p>ISO&nbsp;8601</p> <p>ISO&nbsp;8601</p>
<p>digital archiving</p> <p>digital archiving</p>
</div> </div>
<script type="module"><?= VV::js("public/assets/js/pages/about") ?></script> <script><?= VV::js("public/assets/js/pages/about") ?></script>

View file

@ -1,5 +1,3 @@
import { Hoverpop } from "/assets/js/modules/Hoverpop.mjs";
const randomIntFromInterval = (min, max) => { const randomIntFromInterval = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min); return Math.floor(Math.random() * (max - min + 1) + min);
} }
@ -64,5 +62,17 @@ const implodeInterests = () => {
interestsElement.addEventListener(canHover ? "mouseleave" : "touchend", () => implodeInterests()); interestsElement.addEventListener(canHover ? "mouseleave" : "touchend", () => implodeInterests());
} }
// Languages stacking bar chart hoverpop // Language bar chart hover tooltip
new Hoverpop(document.querySelectorAll("stacked-bar-chart chart-segment")); document.querySelectorAll("stacked-bar-chart chart-segment").forEach(element => {
const tooltipElement = element.querySelector("[data-hover]");
element.addEventListener("mouseenter", () => tooltipElement.classList.add("hovering"));
element.addEventListener("mouseleave", () => tooltipElement.classList.remove("hovering"));
element.addEventListener("mousemove", (event) => {
const x = event.layerX - (tooltipElement.clientWidth / 2);
const y = event.layerY + (tooltipElement.clientHeight - 30);
tooltipElement.style.setProperty("transform", `translate(${x}px, ${y}px)`);
});
});

View file

@ -1,5 +1,3 @@
import { Hoverpop } from "/assets/js/modules/Hoverpop.mjs";
class ContactForm { class ContactForm {
static STORAGE_KEY = "contact_form_message"; static STORAGE_KEY = "contact_form_message";
@ -62,7 +60,16 @@ class ContactForm {
form ? (new ContactForm(form)) : ContactForm.removeSavedMessage(); form ? (new ContactForm(form)) : ContactForm.removeSavedMessage();
} }
// Social links hoverpop document.querySelectorAll("social").forEach(element => {
{ const tooltipElement = element.querySelector("[data-hover]");
new Hoverpop(document.querySelectorAll("social"));
} element.addEventListener("mouseenter", () => tooltipElement.classList.add("hovering"));
element.addEventListener("mouseleave", () => tooltipElement.classList.remove("hovering"));
element.addEventListener("mousemove", (event) => {
const x = event.layerX - (tooltipElement.clientWidth / 2);
const y = event.layerY + tooltipElement.clientHeight;
tooltipElement.style.setProperty("transform", `translate(${x}px, ${y}px)`);
});
});

View file

@ -112,5 +112,4 @@
</button> </button>
</form> </form>
</section> </section>
<script ><?= VV::js("public/assets/js/pages/contact") ?></script>
<script type="module"><?= VV::js("public/assets/js/pages/contact") ?></script>

View file

@ -1,26 +1,22 @@
<?php <?php
use VLW\Database\Models\Search\Search; use VLW\Database\Models\Search\Search;
use const VLW\{ICONS_DIR, DEFAULT_BUTTON_ICON}; use VLW\Database\Tables\Search\{Search as SearchTable, SearchTypeEnum};
use VLW\Database\Tables\Search\{SearchTable, SearchCategoryEnum};
require_once VV::root("src/Consts.php"); require_once VV::root("src/Consts.php");
require_once VV::root("src/Database/Tables/Search/Search.php"); require_once VV::root("src/Database/Tables/Search/Search.php");
require_once VV::root("src/Database/Models/Search/Search.php"); require_once VV::root("src/Database/Models/Search/Search.php");
const LIMIT_RESULTS = 10;
const GET_KEY_QUERY = "q";
$search = new class extends Search { $search = new class extends Search {
public function __construct() {} public readonly string $query;
public readonly array $results;
public static function get_query(): ?string { public function __construct() {
return $_GET[SearchTable::QUERY->value] ?? null; $this->query = $_GET[GET_KEY_QUERY] ?? "";
} $this->results = parent::query($this->query, limit: LIMIT_RESULTS);
public static function get_category(): ?SearchCategoryEnum {
return SearchCategoryEnum::tryFromName($_GET[SearchTable::CATEGORY->value] ?? "");
}
public function search(): array {
return parent::all([SearchTable::QUERY->value => self::get_query()]);
} }
} }
@ -28,54 +24,54 @@
<style><?= VV::css("public/assets/css/pages/search") ?></style> <style><?= VV::css("public/assets/css/pages/search") ?></style>
<section class="search"> <section class="search">
<form> <form>
<input name="<?= SearchTable::QUERY->value ?>" type="search" placeholder="search vlw.se..." value="<?= $search::get_query() ?>"> <input name="<?= GET_KEY_QUERY ?>" type="search" placeholder="search vlw.se..." value="<?= $search->query ?>">
<select name="<?= SearchTable::CATEGORY->value ?>"> <select name="<?= SearchTable::TYPE->value ?>">
<option value="null">All</option> <option value="null">All</option>
<optgroup label="Categories"> <optgroup label="Types">
<?php foreach (SearchCategoryEnum::TABLEs() as $category): ?> <?php foreach (SearchTypeEnum::names() as $type): ?>
<?php $category = SearchCategoryEnum::fromName($category); ?> <?php $type = SearchTypeEnum::fromName($type); ?>
<option value="<?= $category->name ?>" <?= $search::get_category() === $category ? "selected" : "" ?>><?= ucfirst(strtolower($category->name)) ?></option> <option value="<?= $type->name ?>"><?= ucfirst(strtolower($type->name)) ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</optgroup> </optgroup>
</select> </select>
<button type="submit" class="inline solid"><?= VV::embed(ICONS_DIR . "search.svg") ?></button> <button type="submit" class="inline solid"><?= VV::embed("public/assets/media/icons/search.svg") ?></button>
</form> </form>
</section> </section>
<?php if (array_key_exists(SearchTable::QUERY->value, $_GET)): ?> <?php if (array_key_exists(SearchTable::QUERY->value, $_GET)): ?>
<?php if ($search->search()): ?> <?php if ($search->results): ?>
<section class="stats"> <section class="stats">
<p><?= count($search->search()) ?> result(s)</p> <p><?= count($search->results) ?> result(s)</p>
<a href="/search?query=<?= $search::get_query() ?>"><button class="inline solid"> <a href="/search?query=<?= $search->query ?>"><button class="inline solid">
<?= VV::embed(ICONS_DIR . "search.svg") ?> <?= VV::embed("public/assets/media/icons/search.svg") ?>
<p>Advanced search</p> <p>Advanced search</p>
<?= VV::embed(DEFAULT_BUTTON_ICON) ?> <?= VV::embed("public/assets/media/icons/chevron.svg") ?>
</button></a> </button></a>
</section> </section>
<?php foreach ($search->search() as $result): ?> <?php foreach ($search->results as $result): ?>
<section class="result" data-id="<?= $result->id ?>"> <section class="result" data-id="<?= $result->id ?>">
<a href="<?= $result->href() ?>"><button class="inline"> <a href="<?= $result->href ?>"><button class="inline">
<div> <div>
<h2><?= $result->title() ?></h2> <h3><?= $result->title ?></h3>
<p><?= $result->summary() ?></p> <p><?= $result->text ?></p>
</div> </div>
<?= VV::embed(DEFAULT_BUTTON_ICON) ?> <?= VV::embed("public/assets/media/icons/chevron.svg") ?>
</button></a> </button></a>
</section> </section>
<?php endforeach; ?> <?php endforeach; ?>
<?php else: ?> <?php else: ?>
<?php switch (strlen($search::get_query())): default: ?> <?php switch (strlen($search->query)): default: ?>
<section class="stats"> <section class="stats">
<p>0 result(s)</p> <p>0 result(s)</p>
<a href="/search?query=<?= $search::get_query() ?>"><button class="inline solid"> <a href="/search?query=<?= $search->query ?>"><button class="inline solid">
<?= VV::embed(ICONS_DIR . "search.svg") ?> <?= VV::embed("public/assets/media/icons/search.svg") ?>
<p>Advanced search</p> <p>Advanced search</p>
<?= VV::embed(DEFAULT_BUTTON_ICON) ?> <?= VV::embed("public/assets/media/icons/chevron.svg") ?>
</button></a> </button></a>
</section> </section>
<section class="center"> <section class="center">
@ -86,14 +82,14 @@
<?php case 0: ?> <?php case 0: ?>
<section class="center"> <section class="center">
<?= VV::embed(ICONS_DIR . "search.svg") ?> <?= VV::embed("public/assets/media/icons/search.svg") ?>
<p>Start typing to search</p> <p>Start typing to search</p>
</section> </section>
<?php break; ?> <?php break; ?>
<?php case 1: ?> <?php case 1: ?>
<section class="center"> <section class="center">
<?= VV::embed(ICONS_DIR . "search.svg") ?> <?= VV::embed("public/assets/media/icons/search.svg") ?>
<p>Almost, type at least two letters to search</p> <p>Almost, type at least two letters to search</p>
</section> </section>
<?php break; ?> <?php break; ?>
@ -104,7 +100,7 @@
<?php else: ?> <?php else: ?>
<section class="center"> <section class="center">
<?= VV::embed(ICONS_DIR . "search.svg") ?> <?= VV::embed("public/assets/media/icons/search.svg") ?>
<p>Start typing to search</p> <p>Start typing to search</p>
</section> </section>
<?php endif; ?> <?php endif; ?>

View file

@ -135,7 +135,7 @@
<section class="heading"> <section class="heading">
<h1>latest projects</h1> <h1>latest projects</h1>
</section> </section>
<?= VV::include("public/work/timeline?" . http_build_query([TIMELINE_PREVIEW_LIMIT_PARAM => TIMELINE_PREVIEW_LIMIT_COUNT])) ?> <?= VV::include("public/work/timeline", limit: 4) ?>
<section class="heading"> <section class="heading">
<a href="/work/timeline"><button class="inline solid"> <a href="/work/timeline"><button class="inline solid">
<p>view full timeline</p> <p>view full timeline</p>

View file

@ -1,42 +1,35 @@
<?php <?php
use VLW\Database\Models\Work\Timeline; use VLW\Database\Models\Work\Timeline;
use const VLW\{
ICONS_DIR,
DEFAULT_BUTTON_ICON,
TIMELINE_PREVIEW_LIMIT_PARAM
};
require_once VV::root("src/Consts.php");
require_once VV::root("src/Database/Models/Work/Timeline.php"); require_once VV::root("src/Database/Models/Work/Timeline.php");
const ARG_KEY_LIMIT = "limit";
$timeline = new class extends Timeline { $timeline = new class extends Timeline {
public function __construct() {} public function __construct() {}
public static function ordered(): array { public static function ordered(?int $limit = null): array {
// Get timeline list limit from search param if set
$limit = array_key_exists(TIMELINE_PREVIEW_LIMIT_PARAM, $_GET) ? (int) $_GET[TIMELINE_PREVIEW_LIMIT_PARAM] : null;
$timeline = []; $timeline = [];
foreach (parent::all() as $idx => $item) { foreach (parent::all() as $idx => $item) {
// Use year as the first dimension // Use year as the first dimension
if (!array_key_exists($item->year(), $timeline)) { if (!array_key_exists($item->year, $timeline)) {
$timeline[$item->year()] = []; $timeline[$item->year] = [];
} }
// And month as the second dimension // And month as the second dimension
if (!array_key_exists($item->month(), $timeline[$item->year()])) { if (!array_key_exists($item->month, $timeline[$item->year])) {
$timeline[$item->year()][$item->month()] = []; $timeline[$item->year][$item->month] = [];
} }
// Lastly, day as the third dimension // Lastly, day as the third dimension
if (!array_key_exists($item->day(), $timeline[$item->year()][$item->month()])) { if (!array_key_exists($item->day, $timeline[$item->year][$item->month])) {
$timeline[$item->year()][$item->month()][$item->day()] = []; $timeline[$item->year][$item->month][$item->day] = [];
} }
// Append Work instance on Timeline object to the output array by year->month->day // Append Work instance on Timeline object to the output array by year->month->day
$timeline[$item->year()][$item->month()][$item->day()][] = $item->work(); $timeline[$item->year][$item->month][$item->day][] = $item->work;
// Bail out here if we've reached the theshold for items to display // Bail out here if we've reached the theshold for items to display
if ($limit && $idx === $limit) { if ($limit && $idx === $limit) {
@ -67,7 +60,7 @@
<section class="timeline"> <section class="timeline">
<?php // Get year int from key and array of months for current year ?> <?php // Get year int from key and array of months for current year ?>
<?php foreach ($timeline::ordered() as $year => $months): ?> <?php foreach ($timeline::ordered($args[ARG_KEY_LIMIT] ?? null) as $year => $months): ?>
<div class="year"> <div class="year">
<div class="track"> <div class="track">
<p><?= $year ?></p> <p><?= $year ?></p>
@ -100,33 +93,33 @@
<div class="tags"> <div class="tags">
<?php foreach ($work->tags() as $tag): ?> <?php foreach ($work->tags() as $tag): ?>
<p class="tag <?= $tag->label()->name ?>"><?= $tag->label()->name ?></p> <p class="tag <?= $tag->label->name ?>"><?= $tag->label->name ?></p>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if ($work->title()): ?> <?php if ($work->title): ?>
<h2><?= $work->title() ?></h2> <h2><?= $work->title ?></h2>
<?php endif; ?> <?php endif; ?>
<p><?= $work->summary() ?></p> <p><?= $work->summary ?></p>
<?php if ($work->actions()): ?> <?php if ($work->actions()): ?>
<div class="actions"> <div class="actions">
<?php foreach ($work->actions() as $action): ?> <?php foreach ($work->actions() as $action): ?>
<a href="<?= $action->href() ?? "/work/{$work->id}" ?>"><button class="inline <?= implode(" ", $action->classes()) ?>"> <a href="<?= $action->href ?? "/work/{$work->id}" ?>"><button class="inline <?= implode(" ", $action->classlist) ?>">
<?php if ($action->icon_prepended()): ?> <?php if ($action->icon_prepend): ?>
<?= VV::embed(ICONS_DIR . $action->icon_prepended()) ?> <?= VV::embed("public/assets/media/icons/" . $action->icon_prepend) ?>
<?php endif; ?> <?php endif; ?>
<p><?= $action->display_text() ?></p> <p><?= $action->text ?></p>
<?php if ($action->icon_appended()): ?> <?php if ($action->icon_append): ?>
<?= VV::embed(ICONS_DIR . $action->icon_appended()) ?> <?= VV::embed("public/assets/media/icons/" . $action->icon_append) ?>
<?php else: ?> <?php else: ?>
<?= VV::embed(DEFAULT_BUTTON_ICON) ?> <?= VV::embed("public/assets/media/icons/chevron.svg") ?>
<?php endif; ?> <?php endif; ?>
</button></a> </button></a>
<?php endforeach; ?> <?php endforeach; ?>
@ -150,4 +143,3 @@
<?php endforeach; ?> <?php endforeach; ?>
</section> </section>
<script><?= VV::js("assets/js/pages/work/timeline") ?></script>

View file

@ -1,10 +1,3 @@
<?php
use const VLW\DEFAULT_BUTTON_ICON;
require_once VV::root("src/Consts.php");
?>
<style><?= VV::css("public/assets/css/pages/work/wip") ?></style> <style><?= VV::css("public/assets/css/pages/work/wip") ?></style>
<section class="disclaimer"> <section class="disclaimer">
<h1>Soon, very soon!</h1> <h1>Soon, very soon!</h1>
@ -13,6 +6,6 @@
<section class="actions"> <section class="actions">
<a href="/work"><button class="inline"> <a href="/work"><button class="inline">
<p>to featured work</p> <p>to featured work</p>
<?= VV::embed(DEFAULT_BUTTON_ICON) ?> <?= VV::embed("public/assets/media/icons/chevron.svg") ?>
</button></a> </button></a>
</section> </section>

View file

@ -2,20 +2,6 @@
namespace VLW; namespace VLW;
/**
* # Media
* Constants related to media files
*/
const MEDIA_DIR = "/public/assets/media/";
const ICONS_DIR = MEDIA_DIR . "icons/";
const DEFAULT_BUTTON_ICON = ICONS_DIR . "chevron.svg";
/**
* # Search
* Constants for the search API endpoint
*/
const SEARCH_QUERY_MAX_LENGTH = 2048;
/** /**
* # Timeline * # Timeline
* Constants related to the work timeline * Constants related to the work timeline

View file

@ -9,11 +9,12 @@
use VLW\Helpers\UUID; use VLW\Helpers\UUID;
use VLW\Database\Database; use VLW\Database\Database;
use VLW\Database\Models\Model; use VLW\Database\Models\Model;
use VLW\Database\Tables\Coffee\Coffee as CoffeeTable; use VLW\Database\Tables\Coffee\{Stats, Coffee as CoffeeTable};
require_once VV::root("src/Helpers/UUID.php"); require_once VV::root("src/Helpers/UUID.php");
require_once VV::root("src/Database/Database.php"); require_once VV::root("src/Database/Database.php");
require_once VV::root("src/Database/Models/Model.php"); require_once VV::root("src/Database/Models/Model.php");
require_once VV::root("src/Database/Tables/Coffee/Stats.php");
require_once VV::root("src/Database/Tables/Coffee/Coffee.php"); require_once VV::root("src/Database/Tables/Coffee/Coffee.php");
class Coffee extends Model { class Coffee extends Model {
@ -37,6 +38,22 @@
); );
} }
final public static function count_week(): int {
return new Database()
->from(Stats::TABLE)
->limit(1)
->select(Stats::COUNT_WEEK->value)
->fetch_assoc()[Stats::COUNT_WEEK->value] ?? 0;
}
final public static function count_week_average(): int {
return new Database()
->from(Stats::TABLE)
->limit(1)
->select(Stats::COUNT_WEEK_AVERAGE->value)
->fetch_assoc()[Stats::COUNT_WEEK_AVERAGE->value] ?? 0;
}
public function __construct(public readonly string $id) { public function __construct(public readonly string $id) {
parent::__construct(CoffeeTable::TABLE, CoffeeTable::values(), [ parent::__construct(CoffeeTable::TABLE, CoffeeTable::values(), [
CoffeeTable::ID->value => $this->id CoffeeTable::ID->value => $this->id

View file

@ -1,32 +0,0 @@
<?php
namespace VLW\Database\Models\Coffee;
use \VV;
use VLW\API\Endpoints;
use VLW\Database\Models\Model;
use VLW\Database\Tables\Coffee\StatsTable;
require_once VV::root("src/API/Endpoints.php");
require_once VV::root("src/Database/Models/Model.php");
require_once VV::root("src/Database/Tables/Coffee/Stats.php");
require_once VV::root("src/Database/Models/Coffee/Stats.php");
class Stats extends Model {
public function __construct() {
parent::__construct(Endpoints::COFFEE_STATS);
}
public static function all(array $params = []): array {
return [];
}
public function week(): int {
return $this->get(StatsTable::COUNT_WEEK->value) ?? 0;
}
public function week_average(): int {
return $this->get(StatsTable::COUNT_WEEK_AVERAGE->value) ?? 0;
}
}

View file

@ -3,40 +3,69 @@
namespace VLW\Database\Models\Search; namespace VLW\Database\Models\Search;
use \VV; use \VV;
use \vlw\MySQL\Operators;
use VLW\API\Endpoints; use VLW\Helpers\UUID;
use VLW\Database\Database;
use VLW\Database\Models\Model; use VLW\Database\Models\Model;
use VLW\Database\Tables\Search\{SearchTable, SearchCategoryEnum}; use VLW\Database\Tables\Search\{Search as SearchTable, SearchTypeEnum};
require_once VV::root("src/Consts.php"); require_once VV::root("src/Helpers/UUID.php");
require_once VV::root("src/API/Endpoints.php"); require_once VV::root("src/Database/Database.php");
require_once VV::root("src/Database/Models/Model.php"); require_once VV::root("src/Database/Models/Model.php");
require_once VV::root("src/Database/Tables/Search/Search.php"); require_once VV::root("src/Database/Tables/Search/Search.php");
class Search extends Model { class Search extends Model {
final public static function new(string $query, SearchTypeEnum $type, string $title): self {
$id = UUID::v4();
if (!parent::create(SearchTable::TABLE, [
SearchTable::ID->value => $id,
SearchTable::QUERY->value => $query,
SearchTable::TYPE->value => $type->name,
SearchTable::TITLE->value => $title,
SearchTable::TEXT->value => null,
SearchTable::HREF->value => null
])) { throw new Exception("Failed to create Search entity"); }
return new Search($id);
}
final public static function query(string $query, ?int $limit = null): array {
return array_map(fn(array $search): Search => new Search($search[SearchTable::ID->value]), new Database()
->from(SearchTable::TABLE)
->where([SearchTable::QUERY->value => [
Operators::LIKE->value => "%{$query}%"
]])
->limit($limit)
->select(SearchTable::ID->value)
->fetch_all(MYSQLI_ASSOC)
);
}
public function __construct(public readonly string $id) { public function __construct(public readonly string $id) {
parent::__construct(Endpoints::SEARCH, [ parent::__construct(SearchTable::TABLE, SearchTable::values(), [
SearchTable::ID->value => $this->id SearchTable::ID->value => $this->id
]); ]);
} }
public static function all(array $params = []): array { final public string $title {
return array_map(fn(array $item): Search => new Search($item[SearchTable::ID->value]), parent::list(Endpoints::SEARCH, $params)); get => $this->get(SearchTable::TITLE->value);
set (string $title) => $this->set(SearchTable::TITLE->value, $title);
} }
public function title(): ?string { final public ?string $text {
return $this->get(SearchTable::TITLE->value); get => $this->get(SearchTable::TEXT->value);
set (?string $text) => $this->set(SearchTable::TEXT->value, $text);
} }
public function summary(): ?string { final public SearchTypeEnum $type {
return $this->get(SearchTable::SUMMARY->value); get => SearchTypeEnum::fromName($this->get(SearchTable::TYPE->value));
set (SearchTypeEnum $type) => $this->set(SearchTable::TYPE->value, $type->name);
} }
public function category(): ?SearchCategoryEnum { final public string $href {
return SearchCategoryEnum::tryFromName($this->get(SearchTable::CATEGORY->value)); get => $this->get(SearchTable::HREF->value);
} set (string $href) => $this->set(SearchTable::HREF->value, $href);
public function href(): ?string {
return $this->get(SearchTable::HREF->value);
} }
} }

View file

@ -3,13 +3,16 @@
namespace VLW\Database\Models\Work; namespace VLW\Database\Models\Work;
use \VV; use \VV;
use \vlw\MySQL\Order;
use VLW\Helpers\UUID; use VLW\Helpers\UUID;
use VLW\Database\Database;
use VLW\Database\Models\Model; use VLW\Database\Models\Model;
use VLW\Database\Models\Work\Work; use VLW\Database\Models\Work\Work;
use VLW\Database\Tables\Work\Actions; use VLW\Database\Tables\Work\Actions;
require_once VV::root("src/Helpers/UUID.php"); require_once VV::root("src/Helpers/UUID.php");
require_once VV::root("src/Database/Database.php");
require_once VV::root("src/Database/Models/Model.php"); require_once VV::root("src/Database/Models/Model.php");
require_once VV::root("src/Database/Models/Work/Work.php"); require_once VV::root("src/Database/Models/Work/Work.php");
require_once VV::root("src/Database/Tables/Work/Actions.php"); require_once VV::root("src/Database/Tables/Work/Actions.php");
@ -34,7 +37,7 @@
return array_map(fn(array $tag): Actions => new Actions($tag[Actions::ID->value]), new Database() return array_map(fn(array $tag): Actions => new Actions($tag[Actions::ID->value]), new Database()
->from(Actions::TABLE) ->from(Actions::TABLE)
->where([Actions::REF_WORK_ID->value => $work->id]) ->where([Actions::REF_WORK_ID->value => $work->id])
->order([Actions::LABEL->value => Order::DESC]) ->order([Actions::ORDER_IDX->value => Order::DESC])
->select(Actions::ID->value) ->select(Actions::ID->value)
->fetch_all(MYSQLI_ASSOC) ->fetch_all(MYSQLI_ASSOC)
); );
@ -61,6 +64,11 @@
set (?string $href) => $this->set(Actions::HREF->value, $href); set (?string $href) => $this->set(Actions::HREF->value, $href);
} }
final public string $text {
get => $this->get(Actions::TEXT->value);
set (string $text) => $this->set(Actions::TEXT->value, $text);
}
final public ?string $classlist { final public ?string $classlist {
get => $this->get(Actions::CLASSLIST->value); get => $this->get(Actions::CLASSLIST->value);
set (?string $classlist) => $this->set(Actions::CLASSLIST->value, $classlist); set (?string $classlist) => $this->set(Actions::CLASSLIST->value, $classlist);

View file

@ -5,11 +5,13 @@
use \VV; use \VV;
use VLW\Helpers\UUID; use VLW\Helpers\UUID;
use VLW\Database\Database;
use VLW\Database\Models\Model; use VLW\Database\Models\Model;
use VLW\Database\Models\Work\Work; use VLW\Database\Models\Work\Work;
use VLW\Database\Tables\Work\Timeline as TimelineTable; use VLW\Database\Tables\Work\Timeline as TimelineTable;
require_once VV::root("src/Helpers/UUID.php"); require_once VV::root("src/Helpers/UUID.php");
require_once VV::root("src/Database/Database.php");
require_once VV::root("src/Database/Models/Model.php"); require_once VV::root("src/Database/Models/Model.php");
require_once VV::root("src/Database/Models/Work/Work.php"); require_once VV::root("src/Database/Models/Work/Work.php");
require_once VV::root("src/Database/Tables/Work/Timeline.php"); require_once VV::root("src/Database/Tables/Work/Timeline.php");
@ -29,6 +31,14 @@
return new Timeline($id); return new Timeline($id);
} }
final public static function all(): array {
return array_map(fn(array $work): Timeline => new Timeline($work[TimelineTable::ID->value]), new Database()
->from(TimelineTable::TABLE)
->select(TimelineTable::ID->value)
->fetch_all(MYSQLI_ASSOC)
);
}
public function __construct(public readonly string $id) { public function __construct(public readonly string $id) {
parent::__construct(TimelineTable::TABLE, TimelineTable::values(), [ parent::__construct(TimelineTable::TABLE, TimelineTable::values(), [
TimelineTable::ID->value => $this->id TimelineTable::ID->value => $this->id
@ -36,8 +46,8 @@
} }
final public Work $work { final public Work $work {
get => $this->get(WorkTable::REF_WORK_ID->value); get => new Work($this->get(TimelineTable::REF_WORK_ID->value));
set (Work $work) => $this->set(WorkTable::REF_WORK_ID->value, $work->id); set (Work $work) => $this->set(TimelineTable::REF_WORK_ID->value, $work->id);
} }
final public int $year { final public int $year {

View file

@ -12,6 +12,30 @@ CREATE TABLE `coffee` (
`id` char(36) NOT NULL, `id` char(36) NOT NULL,
`date_created` datetime NOT NULL DEFAULT current_timestamp() `date_created` datetime NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
DELIMITER $$
CREATE TRIGGER `coffee_stats_update` AFTER INSERT ON `coffee` FOR EACH ROW BEGIN
DECLARE count_recent INT;
DECLARE count_average INT;
DELETE FROM coffee_stats;
SELECT COUNT(*) INTO count_recent
FROM coffee
WHERE date_created > NOW() - INTERVAL 7 DAY;
SELECT COUNT(*) / COUNT(DISTINCT YEAR(date_created), WEEK(date_created))
INTO count_average
FROM coffee;
INSERT INTO coffee_stats (count_week, count_week_average) VALUES (count_recent, count_average);
END
$$
DELIMITER ;
CREATE TABLE `coffee_stats` (
`count_week` smallint(5) UNSIGNED NOT NULL DEFAULT 0,
`count_week_average` smallint(5) UNSIGNED NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `languages` ( CREATE TABLE `languages` (
`id` char(36) NOT NULL, `id` char(36) NOT NULL,
@ -26,6 +50,15 @@ CREATE TABLE `messages` (
`date_created` datetime NOT NULL DEFAULT current_timestamp() `date_created` datetime NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `search` (
`id` char(36) NOT NULL,
`query` text NOT NULL,
`type` enum('WORK') NOT NULL,
`title` varchar(255) NOT NULL,
`text` text DEFAULT NULL,
`href` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `work` ( CREATE TABLE `work` (
`id` char(36) NOT NULL, `id` char(36) NOT NULL,
`namespace` varchar(255) NOT NULL, `namespace` varchar(255) NOT NULL,
@ -39,6 +72,7 @@ CREATE TABLE `work_actions` (
`ref_work_id` char(36) NOT NULL, `ref_work_id` char(36) NOT NULL,
`order_idx` tinyint(3) UNSIGNED NOT NULL DEFAULT 0, `order_idx` tinyint(3) UNSIGNED NOT NULL DEFAULT 0,
`href` varchar(255) DEFAULT NULL, `href` varchar(255) DEFAULT NULL,
`text` varchar(255) NOT NULL,
`classlist` varchar(255) DEFAULT NULL, `classlist` varchar(255) DEFAULT NULL,
`icon_prepend` varchar(255) DEFAULT NULL, `icon_prepend` varchar(255) DEFAULT NULL,
`icon_append` varchar(255) DEFAULT NULL `icon_append` varchar(255) DEFAULT NULL
@ -69,6 +103,10 @@ ALTER TABLE `languages`
ALTER TABLE `messages` ALTER TABLE `messages`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
ALTER TABLE `search`
ADD PRIMARY KEY (`id`);
ALTER TABLE `search` ADD FULLTEXT KEY `query` (`query`);
ALTER TABLE `work` ALTER TABLE `work`
ADD PRIMARY KEY (`id`), ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `pathname` (`namespace`); ADD UNIQUE KEY `pathname` (`namespace`);

View file

@ -4,7 +4,7 @@
use vlw\xEnum; use vlw\xEnum;
enum SearchCategory { enum SearchTypeEnum {
use xEnum; use xEnum;
case WORK; case WORK;
@ -15,10 +15,10 @@
const TABLE = "search"; const TABLE = "search";
case QUERY = "query"; case ID = "id";
case ID = "id"; case QUERY = "query";
case TITLE = "title"; case TYPE = "type";
case SUMMARY = "summary"; case TITLE = "title";
case CATEGORY = "category"; case TEXT = "text";
case HREF = "href"; case HREF = "href";
} }

View file

@ -13,6 +13,7 @@
case REF_WORK_ID = "ref_work_id"; case REF_WORK_ID = "ref_work_id";
case ORDER_IDX = "order_idx"; case ORDER_IDX = "order_idx";
case HREF = "href"; case HREF = "href";
case TEXT = "text";
case CLASSLIST = "classlist"; case CLASSLIST = "classlist";
case ICON_PREPEND = "icon_prepend"; case ICON_PREPEND = "icon_prepend";
case ICON_APPEND = "icon_append"; case ICON_APPEND = "icon_append";