mirror of
https://codeberg.org/vlw/vlw.se.git
synced 2025-09-13 21:13:40 +02:00
feat: add language chart to about page (#14)
Replaces this section on the `/about` page:  with:  I will replace and fix the colors of the buttons after #15 is merged. Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/14
This commit is contained in:
parent
3b51458dd4
commit
e25b1b6689
13 changed files with 528 additions and 45 deletions
|
@ -5,4 +5,15 @@ pass = ""
|
||||||
|
|
||||||
[databases]
|
[databases]
|
||||||
vlw = ""
|
vlw = ""
|
||||||
battlestation = ""
|
battlestation = ""
|
||||||
|
|
||||||
|
; Forgejo instance config
|
||||||
|
[forgejo]
|
||||||
|
base_url = ""
|
||||||
|
|
||||||
|
; Forgejo language chart endpoints config
|
||||||
|
[about_languages]
|
||||||
|
; CSV of Forgejo profiles to include public source repositories from
|
||||||
|
scan_profiles = ""
|
||||||
|
; Path to a JSON file to store cached language endpoint responses
|
||||||
|
cache_file = ""
|
20
api/composer.lock
generated
20
api/composer.lock
generated
|
@ -8,17 +8,11 @@
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "reflect/plugin-rules",
|
"name": "reflect/plugin-rules",
|
||||||
"version": "1.5.0",
|
"version": "1.5.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/VictorWesterlund/reflect-rules-plugin.git",
|
"url": "https://codeberg.org/reflect/reflect-rules-plugin",
|
||||||
"reference": "9c837fd1944133edfed70a63ce8b32bf67f0d94b"
|
"reference": "df150f0d860dbc2311e5e2fcb2fac36ee52db56b"
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/VictorWesterlund/reflect-rules-plugin/zipball/9c837fd1944133edfed70a63ce8b32bf67f0d94b",
|
|
||||||
"reference": "9c837fd1944133edfed70a63ce8b32bf67f0d94b",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
@ -37,11 +31,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Add request search paramter and request body constraints to an API built with Reflect",
|
"description": "Add request search paramter and request body constraints to an API built with Reflect",
|
||||||
"support": {
|
"time": "2024-11-20T10:39:33+00:00"
|
||||||
"issues": "https://github.com/VictorWesterlund/reflect-rules-plugin/issues",
|
|
||||||
"source": "https://github.com/VictorWesterlund/reflect-rules-plugin/tree/1.5.0"
|
|
||||||
},
|
|
||||||
"time": "2024-01-17T11:07:44+00:00"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "victorwesterlund/xenum",
|
"name": "victorwesterlund/xenum",
|
||||||
|
@ -121,5 +111,5 @@
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": [],
|
"platform": [],
|
||||||
"platform-dev": [],
|
"platform-dev": [],
|
||||||
"plugin-api-version": "2.0.0"
|
"plugin-api-version": "2.3.0"
|
||||||
}
|
}
|
||||||
|
|
22
api/endpoints/about/languages/DELETE.php
Normal file
22
api/endpoints/about/languages/DELETE.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Reflect\Call;
|
||||||
|
use Reflect\Path;
|
||||||
|
use Reflect\Response;
|
||||||
|
|
||||||
|
const MSG_OK = "Cache file deleted";
|
||||||
|
const MSG_FAIL = "Cache file does not exist or can't be deleted";
|
||||||
|
|
||||||
|
class DELETE_AboutLanguages {
|
||||||
|
public function __construct() {}
|
||||||
|
|
||||||
|
// Delete languages cache file if it exists
|
||||||
|
public function main(): Response {
|
||||||
|
// Bail out if cache is not used
|
||||||
|
if (empty($_ENV["forgejo_languages"]["cache_file"])) {
|
||||||
|
return new Response(MSG_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
return unlink($_ENV["forgejo_languages"]["cache_file"]) ? new Response(MSG_OK) : new Response(MSG_FAIL, 404);
|
||||||
|
}
|
||||||
|
}
|
51
api/endpoints/about/languages/GET.php
Normal file
51
api/endpoints/about/languages/GET.php
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Reflect\Call;
|
||||||
|
use Reflect\Path;
|
||||||
|
use Reflect\Response;
|
||||||
|
use ReflectRules\Type;
|
||||||
|
use ReflectRules\Rules;
|
||||||
|
use ReflectRules\Ruleset;
|
||||||
|
|
||||||
|
use VLW\API\Endpoints;
|
||||||
|
|
||||||
|
require_once Path::root("src/Endpoints.php");
|
||||||
|
|
||||||
|
const PARM_FORCE_RECACHE = "force_recache";
|
||||||
|
|
||||||
|
class GET_AboutLanguages {
|
||||||
|
protected Ruleset $ruleset;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->ruleset = new Ruleset(strict: true);
|
||||||
|
|
||||||
|
$this->ruleset->GET([
|
||||||
|
(new Rules(PARM_FORCE_RECACHE))
|
||||||
|
->type(Type::BOOLEAN)
|
||||||
|
->default(false)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->ruleset->validate_or_exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function cache_exists(): bool {
|
||||||
|
return file_exists($_ENV["forgejo_languages"]["cache_file"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function load_cache(): array {
|
||||||
|
return json_decode(file_get_contents($_ENV["forgejo_languages"]["cache_file"]), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function main(): Response {
|
||||||
|
// Delete cache file if force flag is set
|
||||||
|
if ($_GET[PARM_FORCE_RECACHE]) {
|
||||||
|
(new Call(Endpoints::ABOUT_LANGUAGES->value))->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::cache_exists()
|
||||||
|
// Return languages from cache
|
||||||
|
? new Response(self::load_cache())
|
||||||
|
// Fetch and return languages (and generate cache file if enabled)
|
||||||
|
: new Response((new Call(Endpoints::ABOUT_LANGUAGES->value))->post());
|
||||||
|
}
|
||||||
|
}
|
97
api/endpoints/about/languages/POST.php
Normal file
97
api/endpoints/about/languages/POST.php
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Reflect\Call;
|
||||||
|
use Reflect\Path;
|
||||||
|
use Reflect\Response;
|
||||||
|
|
||||||
|
const ERRNO_CACHE_FAILED = 0;
|
||||||
|
|
||||||
|
const FORGEJO_ENDPOINT_USER = "/api/v1/users/%s";
|
||||||
|
const FORGEJO_ENDPOINT_SEARCH = "/api/v1/repos/search?uid=%s";
|
||||||
|
|
||||||
|
class POST_AboutLanguages {
|
||||||
|
private array $errors = [];
|
||||||
|
// Tally of all languages used in all configured repositories
|
||||||
|
private array $languages = [];
|
||||||
|
|
||||||
|
public function __construct() {}
|
||||||
|
|
||||||
|
// Fetch JSON from URL
|
||||||
|
private static function fetch_json(string $url): array {
|
||||||
|
return json_decode(file_get_contents($url), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch JSON from a Forgejo endpoint
|
||||||
|
private static function fetch_endpoint(string $endpoint): array {
|
||||||
|
$url = $_ENV["forgejo"]["base_url"] . $endpoint;
|
||||||
|
return self::fetch_json($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write $this->languages to a JSON file
|
||||||
|
private function cache_languages(): int|bool {
|
||||||
|
$cache_filename = $_ENV["forgejo_languages"]["cache_file"];
|
||||||
|
|
||||||
|
// Bail out if cache file is not configured
|
||||||
|
if (empty($cache_filename)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return file_put_contents($cache_filename, json_encode($this->languages));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch and add languages to total from a fully-qualified Forgejo URL
|
||||||
|
private function add_repository_languages(string $url): void {
|
||||||
|
foreach(self::fetch_json($url) as $language => $bytes) {
|
||||||
|
// Create key for language if it doesn't exist
|
||||||
|
if (!array_key_exists($language, $this->languages)) {
|
||||||
|
$this->languages[$language] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add bytes to language in total
|
||||||
|
$this->languages[$language] += $bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tally languages from public repositories for user id
|
||||||
|
private function add_public_repositores(int $uid): bool {
|
||||||
|
$resp = self::fetch_endpoint(sprintf(FORGEJO_ENDPOINT_SEARCH, $uid));
|
||||||
|
|
||||||
|
// Bail out if request failed or if response indicated a problem
|
||||||
|
if (!$resp or $resp["ok"] === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add langauges for each public repository
|
||||||
|
foreach ($resp["data"] as $repo) {
|
||||||
|
$this->add_repository_languages($repo["languages_url"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add languages from all public repositories for profiles in config
|
||||||
|
private function add_repositories_from_config_profiles(): void {
|
||||||
|
foreach(explode(",", $_ENV["forgejo_languages"]["scan_profiles"]) as $profile) {
|
||||||
|
// Resolve user data from username
|
||||||
|
$user = self::fetch_endpoint(sprintf(FORGEJO_ENDPOINT_USER, $profile));
|
||||||
|
|
||||||
|
if (!$this->add_public_repositores($user["id"])) {
|
||||||
|
$this->errors[] = $profile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function main(): Response {
|
||||||
|
$this->add_repositories_from_config_profiles();
|
||||||
|
|
||||||
|
// Sort langauges bytes tally by largest in descending order
|
||||||
|
arsort($this->languages);
|
||||||
|
|
||||||
|
// Save languages to cache
|
||||||
|
if (!$this->cache_languages()) {
|
||||||
|
$this->errors[] = ERRNO_CACHE_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response($this->languages);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
// Enum of all available VLW endpoints grouped by category
|
// Enum of all available VLW endpoints grouped by category
|
||||||
enum Endpoints: string {
|
enum Endpoints: string {
|
||||||
|
case ABOUT_LANGUAGES = "/about/languages";
|
||||||
|
|
||||||
case SEARCH = "/search";
|
case SEARCH = "/search";
|
||||||
|
|
||||||
case MESSAGES = "/messages";
|
case MESSAGES = "/messages";
|
||||||
|
|
102
public/about.php
102
public/about.php
|
@ -1,12 +1,110 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Vegvisir\Path;
|
||||||
|
use Reflect\Response;
|
||||||
|
|
||||||
|
use VLW\Client\API;
|
||||||
|
use VLW\API\Endpoints;
|
||||||
|
|
||||||
|
require_once VV::root("src/client/API.php");
|
||||||
|
require_once VV::root("api/src/Endpoints.php");
|
||||||
|
|
||||||
|
const BYTE_UNITS = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
|
const FORGEJO_HREF = "https://git.vlw.se/explore/repos?q=&sort=recentupdate&language=";
|
||||||
|
|
||||||
|
$langs = new class extends API {
|
||||||
|
public readonly int $total_bytes;
|
||||||
|
|
||||||
|
private readonly Response $resp;
|
||||||
|
private readonly array $languages;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
// Fetch languages from endpoint
|
||||||
|
$this->resp = $this->call(Endpoints::ABOUT_LANGUAGES->value)->get();
|
||||||
|
|
||||||
|
// We got a response from endpoint
|
||||||
|
if ($this->resp->ok) {
|
||||||
|
$this->languages = $this->resp->json();
|
||||||
|
$this->total_bytes = array_sum($this->languages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all languages as (string) language => (int) language_bytes
|
||||||
|
public function all(): array {
|
||||||
|
return $this->languages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return percent of total for all languages
|
||||||
|
public function get_percent(string $lang, int $mode = PHP_ROUND_HALF_UP): int {
|
||||||
|
return round(($this->languages[$lang] / $this->total_bytes) * 100, 0, $mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return language bytes as percent of whole
|
||||||
|
public function get_percent_str(string $lang): string {
|
||||||
|
$percent = $this->get_percent($lang, PHP_ROUND_HALF_DOWN);
|
||||||
|
return ($percent > 1 ? $percent : "<1") . "%";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return languages bytes as a multiple-byte decimal unit
|
||||||
|
public function get_bytes_str(string $lang): string {
|
||||||
|
$bytes = $this->languages[$lang];
|
||||||
|
|
||||||
|
// Calculate factor for unit
|
||||||
|
$factor = floor((strlen($bytes) - 1) / 3);
|
||||||
|
// Divide by radix 10
|
||||||
|
$format = $bytes / pow(1000, $factor);
|
||||||
|
|
||||||
|
return round($format) . " " . BYTE_UNITS[$factor];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
?>
|
||||||
<style><?= VV::css("public/assets/css/pages/about") ?></style>
|
<style><?= VV::css("public/assets/css/pages/about") ?></style>
|
||||||
<section class="intro">
|
<section class="intro">
|
||||||
<h2 aria-hidden="true">Hi, I'm</h2>
|
<h2 aria-hidden="true">Hi, I"m</h2>
|
||||||
<h1>Victor Westerlund</h1>
|
<h1>Victor Westerlund</h1>
|
||||||
</section>
|
</section>
|
||||||
<hr aria-hidden="true">
|
<hr aria-hidden="true">
|
||||||
<section class="about">
|
<section class="about">
|
||||||
<p>I​'m a full-stack web developer from Sweden.</p>
|
<p>I​'m a full-stack web developer from Sweden.</p>
|
||||||
<p>The <programming/markup/command/query/whatever>-languages I currently use the most are (in a mostly accurate decending order): PHP, JavaScript, CSS, MySQL, TypeScript, Python, SQLite, Bash, and HTML.</p>
|
<p>I used to list the <programming/markup/command/whatever>-languages here that I use the most and order them by guesstimating how much I use each one. But then I thought it would be better to just show you instead using this chart that <a href="https://git.vlw.se/config/vlw.se">automatically pulls the total bytes</a> for each language from my public mirrors and sources on <a href="https://git.vlw.se/vlw">Forgejo</a>.</p>
|
||||||
|
</section>
|
||||||
|
<section class="languages">
|
||||||
|
<stacked-bar-chart>
|
||||||
|
|
||||||
|
<?php foreach ($langs->all() as $lang => $bytes): ?>
|
||||||
|
<a href="<?= FORGEJO_HREF . $lang ?>" target="_blank"><chart-segment style="--size:<?= $langs->get_percent($lang) ?>%;" data-lang="<?= $lang ?>" data-bytes="<?= $bytes ?>">
|
||||||
|
<span data-hover><strong><?= $langs->get_percent_str($lang) ?> <?= $lang ?></strong><br>(<?= $bytes ?> bytes)</span>
|
||||||
|
<!--<p><span><?= $lang ?></span></p>-->
|
||||||
|
</chart-segment></a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
</stacked-bar-chart>
|
||||||
|
<languages-list>
|
||||||
|
|
||||||
|
<?php foreach ($langs->all() as $lang => $bytes): ?>
|
||||||
|
<a href="<?= FORGEJO_HREF . $lang ?>"><language-item data-lang="<?= $lang ?>">
|
||||||
|
<p><?= $langs->get_percent_str($lang) ?></p>
|
||||||
|
<p class="lang"><?= $lang ?></p>
|
||||||
|
<p><?= $langs->get_bytes_str($lang) ?></p>
|
||||||
|
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
|
||||||
|
</language-item></a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
</languages-list>
|
||||||
|
<stacked-bar-chart>
|
||||||
|
|
||||||
|
<?php foreach ($langs->all() as $lang => $bytes): ?>
|
||||||
|
<a href="<?= FORGEJO_HREF . $lang ?>" target="_blank"><chart-segment style="--size:<?= $langs->get_percent($lang) ?>%;" data-lang="<?= $lang ?>" data-bytes="<?= $bytes ?>">
|
||||||
|
<span data-hover><strong><?= $langs->get_percent_str($lang) ?> <?= $lang ?></strong><br>(<?= $bytes ?> bytes)</span>
|
||||||
|
<!--<p><span><?= $lang ?></span></p>-->
|
||||||
|
</chart-segment></a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
</stacked-bar-chart>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
<section class="about">
|
<section class="about">
|
||||||
<h2>This website</h2>
|
<h2>This website</h2>
|
||||||
|
|
|
@ -3,6 +3,15 @@
|
||||||
:root {
|
:root {
|
||||||
--primer-color-accent: 148, 255, 21;
|
--primer-color-accent: 148, 255, 21;
|
||||||
--color-accent: rgb(var(--primer-color-accent));
|
--color-accent: rgb(var(--primer-color-accent));
|
||||||
|
|
||||||
|
--color-go: rgb(0, 173, 216);
|
||||||
|
--color-php: rgb(79, 93, 149);
|
||||||
|
--color-css: rgb(86, 61, 124);
|
||||||
|
--color-html: rgb(227, 76, 38);
|
||||||
|
--color-shell: rgb(137, 224, 81);
|
||||||
|
--color-python: rgb(53, 114, 165);
|
||||||
|
--color-typescript: rgb(49, 120, 198);
|
||||||
|
--color-javascript: rgb(241, 224, 90);
|
||||||
}
|
}
|
||||||
|
|
||||||
vv-shell {
|
vv-shell {
|
||||||
|
@ -45,6 +54,147 @@ section.about p i:not(:hover) {
|
||||||
opacity: .3;
|
opacity: .3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ## Languages */
|
||||||
|
|
||||||
|
section.languages {
|
||||||
|
margin: calc(var(--padding) / 1.5) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart {
|
||||||
|
gap: 3px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 100px;
|
||||||
|
height: var(--padding);
|
||||||
|
background-color: rgba(255, 255, 255, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart:last-of-type {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart:hover chart-segment {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart chart-segment {
|
||||||
|
--border-corner-radius: 100px;
|
||||||
|
|
||||||
|
transition: 150ms opacity;
|
||||||
|
width: var(--size, 0%);
|
||||||
|
min-width: 3%;
|
||||||
|
height: 100%;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba(255, 255, 255, .1);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart a:nth-child(odd) chart-segment {
|
||||||
|
background-color: rgba(255, 255, 255, .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ### Round corners */
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart a:first-child chart-segment {
|
||||||
|
border-top-right-radius: var(--padding);
|
||||||
|
border-bottom-right-radius: var(--padding);
|
||||||
|
border-top-left-radius: var(--border-corner-radius);
|
||||||
|
border-bottom-left-radius: var(--border-corner-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart a:last-child chart-segment {
|
||||||
|
border-top-left-radius: var(--padding);
|
||||||
|
border-bottom-left-radius: var(--padding);
|
||||||
|
border-top-right-radius: var(--border-corner-radius);
|
||||||
|
border-bottom-right-radius: var(--border-corner-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart:last-of-type a:first-child chart-segment {
|
||||||
|
border-top-left-radius: var(--padding);
|
||||||
|
border-bottom-left-radius: var(--padding);
|
||||||
|
border-top-right-radius: var(--border-corner-radius);
|
||||||
|
border-bottom-right-radius: var(--border-corner-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart:last-of-type a:last-child chart-segment {
|
||||||
|
border-top-right-radius: var(--padding);
|
||||||
|
border-bottom-right-radius: var(--padding);
|
||||||
|
border-top-left-radius: var(--border-corner-radius);
|
||||||
|
border-bottom-left-radius: var(--border-corner-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ### Texts */
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart chart-segment p {
|
||||||
|
text-align: center;
|
||||||
|
color: inherit;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart chart-segment[style="--size:0%;"] p span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart chart-segment[style="--size:0%;"] p::before {
|
||||||
|
content: "<1%";
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart chart-segment [data-hover] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ### Colors */
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart a chart-segment[data-lang="Go"] { background-color: var(--color-go); }
|
||||||
|
section.languages stacked-bar-chart a chart-segment[data-lang="PHP"] { background-color: var(--color-php); }
|
||||||
|
section.languages stacked-bar-chart a chart-segment[data-lang="CSS"] { background-color: var(--color-css); }
|
||||||
|
section.languages stacked-bar-chart a chart-segment[data-lang="HTML"] { background-color: var(--color-html); }
|
||||||
|
section.languages stacked-bar-chart a chart-segment[data-lang="Python"] { background-color: var(--color-python); }
|
||||||
|
section.languages stacked-bar-chart a chart-segment[data-lang="TypeScript"] { background-color: var(--color-typescript); }
|
||||||
|
section.languages stacked-bar-chart a chart-segment[data-lang="Shell"] { background-color: var(--color-shell); color: black; }
|
||||||
|
section.languages stacked-bar-chart a chart-segment[data-lang="JavaScript"] { background-color: var(--color-javascript); color: black; }
|
||||||
|
|
||||||
|
/* ### Legend */
|
||||||
|
|
||||||
|
section.languages languages-list {
|
||||||
|
gap: calc(var(--padding) / 2);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
margin: var(--padding) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages languages-list language-item {
|
||||||
|
gap: 10px;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 8px;
|
||||||
|
align-items: center;
|
||||||
|
fill: var(--color-php);
|
||||||
|
padding: calc(var(--padding) / 1.5);
|
||||||
|
border: solid 1px rgba(255, 255, 255, .1);
|
||||||
|
background: linear-gradient(139deg, rgba(0, 0, 0, 0) 0%, rgba(79, 93, 144, .2) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages languages-list language-item p.lang {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: 900;
|
||||||
|
color: var(--color-php);
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages languages-list language-item svg {
|
||||||
|
width: 2em;
|
||||||
|
margin-left: auto;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
/* # Interests */
|
/* # Interests */
|
||||||
|
|
||||||
div.interests {
|
div.interests {
|
||||||
|
@ -85,4 +235,47 @@ div.interests p {
|
||||||
-webkit-filter: hue-rotate(360deg);
|
-webkit-filter: hue-rotate(360deg);
|
||||||
filter: hue-rotate(360deg);
|
filter: hue-rotate(360deg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Feature queries */
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
section.languages stacked-bar-chart chart-segment:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart chart-segment [data-hover] {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
text-align: center;
|
||||||
|
transform: translate(0, 0);
|
||||||
|
background-color: inherit;
|
||||||
|
padding: 5px 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
z-index: 2000;
|
||||||
|
-webkit-backdrop-filter: brightness(.2) blur(20px);
|
||||||
|
backdrop-filter: brightness(.2) blur(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
section.languages stacked-bar-chart chart-segment [data-hover].hovering {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Size queries */
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
section.languages languages-list {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 650px) {
|
||||||
|
section.languages languages-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -71,7 +71,7 @@ section.social social:hover {
|
||||||
fill: var(--color-accent);
|
fill: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
section.social social.hovering p {
|
section.social social p.hovering {
|
||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
32
public/assets/js/modules/Hoverpop.mjs
Normal file
32
public/assets/js/modules/Hoverpop.mjs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
|
||||||
|
|
||||||
|
export const TARGET_SELECTOR = "[data-hover]";
|
||||||
|
|
||||||
|
export class Hoverpop {
|
||||||
|
/**
|
||||||
|
* Bind hover targets on provided Elevent-compatible element(s)
|
||||||
|
* @param {HTMLElement|HTMLElements|string} elements
|
||||||
|
*/
|
||||||
|
constructor(elements) {
|
||||||
|
// Bind hover targets on element(s)
|
||||||
|
new Elevent("mouseenter", elements, (event) => {
|
||||||
|
const element = event.target.querySelector(TARGET_SELECTOR);
|
||||||
|
|
||||||
|
// Bail out if target element contains no hover target element
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.classList.add("hovering");
|
||||||
|
event.target.addEventListener("mousemove", (event) => {
|
||||||
|
const x = event.layerX - (element.clientWidth / 2);
|
||||||
|
const y = event.layerY + element.clientHeight;
|
||||||
|
|
||||||
|
element.style.setProperty("transform", `translate(${x}px, ${y}px)`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind hover leave targets on element(s)
|
||||||
|
new Elevent("mouseleave", elements, () => elements.forEach(element => element.querySelector(TARGET_SELECTOR)?.classList.remove("hovering")));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
@ -65,3 +67,6 @@ const implodeInterests = () => {
|
||||||
|
|
||||||
interestsElement.addEventListener(canHover ? "mouseleave" : "touchend", () => implodeInterests());
|
interestsElement.addEventListener(canHover ? "mouseleave" : "touchend", () => implodeInterests());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Languages stacking bar chart hoverpop
|
||||||
|
new Hoverpop(document.querySelectorAll("stacked-bar-chart chart-segment"));
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Hoverpop } from "/assets/js/modules/Hoverpop.mjs";
|
||||||
|
|
||||||
class ContactForm {
|
class ContactForm {
|
||||||
static STORAGE_KEY = "contact_form_message";
|
static STORAGE_KEY = "contact_form_message";
|
||||||
|
|
||||||
|
@ -60,27 +62,7 @@ class ContactForm {
|
||||||
form ? (new ContactForm(form)) : ContactForm.removeSavedMessage();
|
form ? (new ContactForm(form)) : ContactForm.removeSavedMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Social links hover
|
// Social links hoverpop
|
||||||
{
|
{
|
||||||
const socialElementHover = (target) => {
|
new Hoverpop(document.querySelectorAll("social"));
|
||||||
const element = target.querySelector("p");
|
|
||||||
|
|
||||||
target.classList.add("hovering");
|
|
||||||
target.addEventListener("mousemove", (event) => {
|
|
||||||
const x = event.layerX - (element.clientWidth / 2);
|
|
||||||
const y = event.layerY + element.clientHeight;
|
|
||||||
|
|
||||||
element.style.setProperty("transform", `translate(${x}px, ${y}px)`);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const elements = [...document.querySelectorAll("social")];
|
|
||||||
|
|
||||||
elements.forEach(element => {
|
|
||||||
element.addEventListener("mouseenter", () => socialElementHover(element));
|
|
||||||
|
|
||||||
element.addEventListener("mouseleave", () => {
|
|
||||||
elements.forEach(element => element.classList.remove("hovering"));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
|
@ -63,15 +63,15 @@
|
||||||
<section class="social">
|
<section class="social">
|
||||||
<a href="mailto:victor@vlw.se"><social>
|
<a href="mailto:victor@vlw.se"><social>
|
||||||
<?= VV::embed("public/assets/media/icons/email.svg") ?>
|
<?= VV::embed("public/assets/media/icons/email.svg") ?>
|
||||||
<p>e-mail</p>
|
<p data-hover>e-mail</p>
|
||||||
</social></a>
|
</social></a>
|
||||||
<a href="matrix:@vlw:vlw.se"><social>
|
<a href="matrix:@vlw:vlw.se"><social>
|
||||||
<?= VV::embed("public/assets/media/icons/matrix.svg") ?>
|
<?= VV::embed("public/assets/media/icons/matrix.svg") ?>
|
||||||
<p>matrix</p>
|
<p data-hover>matrix</p>
|
||||||
</social></a>
|
</social></a>
|
||||||
<a href="https://web.libera.chat/#vlw.se"><social>
|
<a href="https://web.libera.chat/#vlw.se"><social>
|
||||||
<?= VV::embed("public/assets/media/icons/libera.svg") ?>
|
<?= VV::embed("public/assets/media/icons/libera.svg") ?>
|
||||||
<p>libera.chat</p>
|
<p data-hover>libera.chat</p>
|
||||||
</social></a>
|
</social></a>
|
||||||
</section>
|
</section>
|
||||||
<?= VV::embed("public/assets/media/line.svg") ?>
|
<?= VV::embed("public/assets/media/line.svg") ?>
|
||||||
|
@ -133,4 +133,4 @@
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script><?= VV::js("public/assets/js/pages/contact") ?></script>
|
<script type="module"><?= VV::js("public/assets/js/pages/contact") ?></script>
|
Loading…
Add table
Reference in a new issue