mirror of
https://codeberg.org/vlw/vlw.se.git
synced 2025-09-13 13:03:41 +02:00
wip: 2025-07-31T07:36:19+0200 (1753940179)
This commit is contained in:
parent
eb2c7b7d82
commit
c27df3d946
17 changed files with 249 additions and 190 deletions
|
@ -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 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>
|
|
@ -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)`);
|
||||
});
|
||||
});
|
|
@ -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)`);
|
||||
});
|
||||
});
|
|
@ -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>
|
|
@ -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; ?>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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`);
|
||||
|
|
|
@ -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";
|
||||
}
|
|
@ -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";
|
||||
|
|
Loading…
Add table
Reference in a new issue