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
use VLW\Database\Models\Coffee\Stats;
use VLW\Database\Models\About\Language;
use const VLW\{
FORGEJO_HREF,
FORGEJO_SI_BYTE_MULTIPLE,
DEFAULT_BUTTON_ICON
};
use VLW\Database\Models\Coffee\Coffee;
use VLW\Database\Models\Languages\Language;
require_once VV::root("src/Consts.php");
require_once VV::root("src/Database/Models/Coffee/Stats.php");
require_once VV::root("src/Database/Models/About/Language.php");
require_once VV::root("src/Database/Models/Coffee/Coffee.php");
require_once VV::root("src/Database/Models/Languages/Language.php");
const FORGEJO = "https://git.vlw.se/explore/repos?language=";
const SI_BYTE_MULTIPLE = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
$languages = new class extends Language {
private readonly int $total_bytes;
public function __construct() {
$this->total_bytes = array_sum(array_map(fn(Language $language): int => $language->bytes(), parent::all()));
$this->total_bytes = array_sum(array_map(fn(Language $language): int => $language->bytes, parent::all()));
}
public function percent(Language $language, int $mode = PHP_ROUND_HALF_UP): int {
return round(($language->bytes() / $this->total_bytes) * 100, 0, $mode);
return round(($language->bytes / $this->total_bytes) * 100, 0, $mode);
}
public function percent_string(Language $language): string {
@ -29,17 +26,25 @@
public function bytes_si_string(Language $language): string {
// Calculate factor for unit
$factor = floor((strlen($language->bytes()) - 1) / 3);
$factor = floor((strlen($language->bytes) - 1) / 3);
// 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 {
$diff = $this->week() - $this->week_average();
$diff = $this->count_week - $this->count_week_average;
return match (true) {
$diff < 0 => "less than",
@ -65,8 +70,8 @@
<stacked-bar-chart>
<?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() ?>">
<span data-hover><strong><?= $languages->percent_string($language) ?> <?= $language->id ?></strong><br>(<?= $language->bytes() ?> bytes)</span>
<a href="<?= FORGEJO . $language->name ?>" target="_blank"><chart-segment style="--size:<?= $languages->percent($language) ?>%;" data-lang="<?= $language->name ?>" data-bytes="<?= $language->bytes ?>">
<span data-hover><strong><?= $languages->percent_string($language) ?> <?= $language->name ?></strong><br>(<?= $language->bytes ?> bytes)</span>
</chart-segment></a>
<?php endforeach; ?>
@ -74,11 +79,11 @@
<languages-list>
<?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 class="lang"><?= $language->id ?></p>
<p class="lang"><?= $language->name ?></p>
<p><?= $languages->bytes_si_string($language) ?></p>
<?= VV::embed(DEFAULT_BUTTON_ICON) ?>
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
</button></a>
<?php endforeach; ?>
@ -86,8 +91,8 @@
<stacked-bar-chart>
<?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() ?>">
<span data-hover><strong><?= $languages->percent_string($language) ?> <?= $language->id ?></strong><br>(<?= $language->bytes() ?> bytes)</span>
<a href="<?= FORGEJO . $language->name ?>" target="_blank"><chart-segment style="--size:<?= $languages->percent($language) ?>%;" data-lang="<?= $language->name ?>" data-bytes="<?= $language->bytes ?>">
<span data-hover><strong><?= $languages->percent_string($language) ?> <?= $language->name ?></strong><br>(<?= $language->bytes ?> bytes)</span>
</chart-segment></a>
<?php endforeach; ?>
@ -101,7 +106,7 @@
</section>
<section class="about">
<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>Another silent passion of mine that comes out every few years is building computers and fiddling with networking stuff.</p>
</section>
@ -132,4 +137,4 @@
<p>ISO&nbsp;8601</p>
<p>digital archiving</p>
</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) => {
return Math.floor(Math.random() * (max - min + 1) + min);
}
@ -64,5 +62,17 @@ const implodeInterests = () => {
interestsElement.addEventListener(canHover ? "mouseleave" : "touchend", () => implodeInterests());
}
// Languages stacking bar chart hoverpop
new Hoverpop(document.querySelectorAll("stacked-bar-chart chart-segment"));
// Language bar chart hover tooltip
document.querySelectorAll("stacked-bar-chart chart-segment").forEach(element => {
const tooltipElement = element.querySelector("[data-hover]");
element.addEventListener("mouseenter", () => tooltipElement.classList.add("hovering"));
element.addEventListener("mouseleave", () => tooltipElement.classList.remove("hovering"));
element.addEventListener("mousemove", (event) => {
const x = event.layerX - (tooltipElement.clientWidth / 2);
const y = event.layerY + (tooltipElement.clientHeight - 30);
tooltipElement.style.setProperty("transform", `translate(${x}px, ${y}px)`);
});
});

View file

@ -1,5 +1,3 @@
import { Hoverpop } from "/assets/js/modules/Hoverpop.mjs";
class ContactForm {
static STORAGE_KEY = "contact_form_message";
@ -62,7 +60,16 @@ class ContactForm {
form ? (new ContactForm(form)) : ContactForm.removeSavedMessage();
}
// Social links hoverpop
{
new Hoverpop(document.querySelectorAll("social"));
}
document.querySelectorAll("social").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;
tooltipElement.style.setProperty("transform", `translate(${x}px, ${y}px)`);
});
});

View file

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

View file

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

View file

@ -135,7 +135,7 @@
<section class="heading">
<h1>latest projects</h1>
</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">
<a href="/work/timeline"><button class="inline solid">
<p>view full timeline</p>

View file

@ -1,42 +1,35 @@
<?php
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");
const ARG_KEY_LIMIT = "limit";
$timeline = new class extends Timeline {
public function __construct() {}
public static function ordered(): 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;
public static function ordered(?int $limit = null): array {
$timeline = [];
foreach (parent::all() as $idx => $item) {
// Use year as the first dimension
if (!array_key_exists($item->year(), $timeline)) {
$timeline[$item->year()] = [];
if (!array_key_exists($item->year, $timeline)) {
$timeline[$item->year] = [];
}
// And month as the second dimension
if (!array_key_exists($item->month(), $timeline[$item->year()])) {
$timeline[$item->year()][$item->month()] = [];
if (!array_key_exists($item->month, $timeline[$item->year])) {
$timeline[$item->year][$item->month] = [];
}
// Lastly, day as the third dimension
if (!array_key_exists($item->day(), $timeline[$item->year()][$item->month()])) {
$timeline[$item->year()][$item->month()][$item->day()] = [];
if (!array_key_exists($item->day, $timeline[$item->year][$item->month])) {
$timeline[$item->year][$item->month][$item->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
if ($limit && $idx === $limit) {
@ -67,7 +60,7 @@
<section class="timeline">
<?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="track">
<p><?= $year ?></p>
@ -100,33 +93,33 @@
<div class="tags">
<?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; ?>
</div>
<?php endif; ?>
<?php if ($work->title()): ?>
<h2><?= $work->title() ?></h2>
<?php if ($work->title): ?>
<h2><?= $work->title ?></h2>
<?php endif; ?>
<p><?= $work->summary() ?></p>
<p><?= $work->summary ?></p>
<?php if ($work->actions()): ?>
<div class="actions">
<?php foreach ($work->actions() as $action): ?>
<a href="<?= $action->href() ?? "/work/{$work->id}" ?>"><button class="inline <?= implode(" ", $action->classes()) ?>">
<?php if ($action->icon_prepended()): ?>
<?= VV::embed(ICONS_DIR . $action->icon_prepended()) ?>
<a href="<?= $action->href ?? "/work/{$work->id}" ?>"><button class="inline <?= implode(" ", $action->classlist) ?>">
<?php if ($action->icon_prepend): ?>
<?= VV::embed("public/assets/media/icons/" . $action->icon_prepend) ?>
<?php endif; ?>
<p><?= $action->display_text() ?></p>
<p><?= $action->text ?></p>
<?php if ($action->icon_appended()): ?>
<?= VV::embed(ICONS_DIR . $action->icon_appended()) ?>
<?php if ($action->icon_append): ?>
<?= VV::embed("public/assets/media/icons/" . $action->icon_append) ?>
<?php else: ?>
<?= VV::embed(DEFAULT_BUTTON_ICON) ?>
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
<?php endif; ?>
</button></a>
<?php endforeach; ?>
@ -149,5 +142,4 @@
</div>
<?php endforeach; ?>
</section>
<script><?= VV::js("assets/js/pages/work/timeline") ?></script>
</section>

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>
<section class="disclaimer">
<h1>Soon, very soon!</h1>
@ -13,6 +6,6 @@
<section class="actions">
<a href="/work"><button class="inline">
<p>to featured work</p>
<?= VV::embed(DEFAULT_BUTTON_ICON) ?>
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
</button></a>
</section>

View file

@ -2,20 +2,6 @@
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
* Constants related to the work timeline

View file

@ -9,11 +9,12 @@
use VLW\Helpers\UUID;
use VLW\Database\Database;
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/Database/Database.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");
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) {
parent::__construct(CoffeeTable::TABLE, CoffeeTable::values(), [
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;
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\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/API/Endpoints.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/Tables/Search/Search.php");
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) {
parent::__construct(Endpoints::SEARCH, [
parent::__construct(SearchTable::TABLE, SearchTable::values(), [
SearchTable::ID->value => $this->id
]);
}
public static function all(array $params = []): array {
return array_map(fn(array $item): Search => new Search($item[SearchTable::ID->value]), parent::list(Endpoints::SEARCH, $params));
final public string $title {
get => $this->get(SearchTable::TITLE->value);
set (string $title) => $this->set(SearchTable::TITLE->value, $title);
}
public function title(): ?string {
return $this->get(SearchTable::TITLE->value);
final public ?string $text {
get => $this->get(SearchTable::TEXT->value);
set (?string $text) => $this->set(SearchTable::TEXT->value, $text);
}
public function summary(): ?string {
return $this->get(SearchTable::SUMMARY->value);
final public SearchTypeEnum $type {
get => SearchTypeEnum::fromName($this->get(SearchTable::TYPE->value));
set (SearchTypeEnum $type) => $this->set(SearchTable::TYPE->value, $type->name);
}
public function category(): ?SearchCategoryEnum {
return SearchCategoryEnum::tryFromName($this->get(SearchTable::CATEGORY->value));
}
public function href(): ?string {
return $this->get(SearchTable::HREF->value);
final public string $href {
get => $this->get(SearchTable::HREF->value);
set (string $href) => $this->set(SearchTable::HREF->value, $href);
}
}

View file

@ -3,13 +3,16 @@
namespace VLW\Database\Models\Work;
use \VV;
use \vlw\MySQL\Order;
use VLW\Helpers\UUID;
use VLW\Database\Database;
use VLW\Database\Models\Model;
use VLW\Database\Models\Work\Work;
use VLW\Database\Tables\Work\Actions;
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/Work/Work.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()
->from(Actions::TABLE)
->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)
->fetch_all(MYSQLI_ASSOC)
);
@ -61,6 +64,11 @@
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 {
get => $this->get(Actions::CLASSLIST->value);
set (?string $classlist) => $this->set(Actions::CLASSLIST->value, $classlist);

View file

@ -5,11 +5,13 @@
use \VV;
use VLW\Helpers\UUID;
use VLW\Database\Database;
use VLW\Database\Models\Model;
use VLW\Database\Models\Work\Work;
use VLW\Database\Tables\Work\Timeline as TimelineTable;
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/Work/Work.php");
require_once VV::root("src/Database/Tables/Work/Timeline.php");
@ -29,6 +31,14 @@
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) {
parent::__construct(TimelineTable::TABLE, TimelineTable::values(), [
TimelineTable::ID->value => $this->id
@ -36,8 +46,8 @@
}
final public Work $work {
get => $this->get(WorkTable::REF_WORK_ID->value);
set (Work $work) => $this->set(WorkTable::REF_WORK_ID->value, $work->id);
get => new Work($this->get(TimelineTable::REF_WORK_ID->value));
set (Work $work) => $this->set(TimelineTable::REF_WORK_ID->value, $work->id);
}
final public int $year {

View file

@ -12,6 +12,30 @@ CREATE TABLE `coffee` (
`id` char(36) NOT NULL,
`date_created` datetime NOT NULL DEFAULT current_timestamp()
) 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` (
`id` char(36) NOT NULL,
@ -26,6 +50,15 @@ CREATE TABLE `messages` (
`date_created` datetime NOT NULL DEFAULT current_timestamp()
) 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` (
`id` char(36) NOT NULL,
`namespace` varchar(255) NOT NULL,
@ -39,6 +72,7 @@ CREATE TABLE `work_actions` (
`ref_work_id` char(36) NOT NULL,
`order_idx` tinyint(3) UNSIGNED NOT NULL DEFAULT 0,
`href` varchar(255) DEFAULT NULL,
`text` varchar(255) NOT NULL,
`classlist` varchar(255) DEFAULT NULL,
`icon_prepend` varchar(255) DEFAULT NULL,
`icon_append` varchar(255) DEFAULT NULL
@ -69,6 +103,10 @@ ALTER TABLE `languages`
ALTER TABLE `messages`
ADD PRIMARY KEY (`id`);
ALTER TABLE `search`
ADD PRIMARY KEY (`id`);
ALTER TABLE `search` ADD FULLTEXT KEY `query` (`query`);
ALTER TABLE `work`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `pathname` (`namespace`);

View file

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

View file

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