Compare commits

...

3 commits

Author SHA1 Message Date
a6c74f5c4f feat: add web highlights section to work page (#34)
This PR adds a "web highlights" section to the work page.

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/34
2025-03-29 07:34:54 +00:00
f60a27855d feat: add work references and remove default work timeline button (#33)
Added references to some new repos that I've created
- [vlw/big-black-coffee-button](https://codeberg.org/vlw/big-black-coffee-button)
- [vlw/href](https://codeberg.org/vlw/href)
- [vlw/curl](https://codeberg.org/vlw/curl)

I've also removed the 'read more' button that showed up in the work timeline for items that didn't have any actions defined. It's probably better to have no "read more" button at all than having it link to a "coming soon" page.

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/33
2025-03-29 07:34:39 +00:00
c5c7aaa919 fix(content): change about-me page texts and fix interest anim origin (#32)
More changes to the about page texts and also made the "interests explosion" effect center on pointer position instead of center of span, which caused some glitchy looking behavior when the text wraps. Also added "digital archiving" to the interests list!

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/32
2025-03-29 07:34:14 +00:00
13 changed files with 120 additions and 63 deletions

View file

@ -42,9 +42,9 @@
$diff = $this->week() - $this->week_average();
return match (true) {
$diff < 1 => "less than",
$diff === 1 => "the same as",
$diff > 1 => "more than"
$diff < 0 => "less than",
$diff === 0 => "the same as",
$diff > 0 => "more than"
};
}
};
@ -52,14 +52,14 @@
?>
<style><?= VV::css("public/assets/css/pages/about") ?></style>
<section class="intro">
<h2 aria-hidden="true">Hi, I"m</h2>
<h2 aria-hidden="true">Hi, I'm</h2>
<h1>Victor Westerlund</h1>
</section>
<hr aria-hidden="true">
<section class="about">
<p>I&ZeroWidthSpace;'m a full-stack web developer from Sweden, and welcome to my little personal corner of the Internet!</p>
<p>I used to list the &lt;programming/markup/command/whatever&gt;-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 automatically pulls the total bytes for each language from my public repos on <a href="https://git.vlw.se/vlw">Forgejo</a>.</p>
<p>Some other noteworthy techologies that I work a decent amount with are: Debian, MariaDB, SQLite, DNS, Redis, and probably a few others as well. Check out this page for a comprehensive list of all the tech that I use.</p>
<p>My coding happens almost exclusivly in <a href="https://github.com/coder/code-server">code-server</a>, which is a fork of VSCode that runs entirely in the browser. I keep my development environment tucked away in a lightweight Debian VA that I can tote around to whatever host machine I happen to work on. I also keep an ephemeral Debian Live ISO ready which boots into a VM RAM disk where I can mess around without fear or breaking things or try new software.</p>
<p>I used to list the &lt;programming/markup/command/whatever&gt;-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 automatically pulls the total bytes for each language from my <a href="https://git.vlw.se/explore/repos">public repos on Forgejo</a>.</p>
</section>
<section class="languages">
<stacked-bar-chart>
@ -101,18 +101,10 @@
</section>
<section class="about">
<h2>Personal</h2>
<p>One thing is true.. 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->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>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>My coding happens almost exclusivly in <a href="https://github.com/coder/code-server">code-server</a>, which is a fork of VSCode that runs entirely in the browser. I keep my development environment tucked away in a lightweight Debian VA that I can tote around to whatever host machine I happen to work on. If I can't do that for whatever reason, I have my <a href="https://codeberg.org/vlw/dotfiles">dotfiles</a> ready to get things set up the way I like it.</p>
<p>Another silent passion of mine that comes out every few years is building computers and fiddling with networking stuff.</p>
</section>
<section class="about">
<h2>Projects</h2>
<p>Here are some projects I'm working on right now:</p>
<p>* <a href="https://vegvisir.vlw.se">Vegvisir</a>: A web navigation framework for PHP.</p>
<p>* <a href="https://reflect.vlw.se">Reflect</a>: A REST API framework for PHP developers.</p>
<p>Check out this <a href="/work/timeline">timeline</a> for a somewhat complete list of everything I have done.</p>
</section>
<hr>
<section class="about">
<h3>GitHub</h3>
@ -138,5 +130,6 @@
<p>photography</p>
<p>videography</p>
<p>ISO&nbsp;8601</p>
<p>digital archiving</p>
</div>
<script type="module"><?= VV::js("public/assets/js/pages/about") ?></script>

View file

@ -128,19 +128,28 @@ section.featured featured-item .title svg {
fill: var(--color-accent);
}
/* ### Languages */
/* ### Actions */
section.featured featured-item img {
width: 100%;
border-radius: 4px;
}
section.featured featured-item .actions {
gap: 5px;
gap: var(--padding);
width: 100%;
display: flex;
flex-direction: column;
padding-top: var(--padding);
margin-top: auto;
}
/* # Size queries */
@media (min-width: 400px) {
section.featured featured-item .actions {
flex-direction: row;
}
}
@media (min-width: 600px) {
section.hero {
grid-template-columns: repeat(2, 1fr);
@ -151,4 +160,8 @@ section.featured featured-item .actions {
section.featured {
grid-template-columns: repeat(3, 1fr);
}
section.featured featured-item .actions button.collapse p {
display: none;
}
}

View file

@ -0,0 +1,16 @@
/* # Overrides */
:root {
--primer-color-accent: 3, 255, 219;
--color-accent: rgb(var(--primer-color-accent));
--hue-accent: 90deg;
}
vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
width: 100%;
max-width: 1200px;
overflow-x: initial;
}

View file

@ -58,13 +58,9 @@ const implodeInterests = () => {
// Bind mouse or touch events depending on pointer type of device
const canHover = window.matchMedia("(pointer: fine)").matches;
interestsElement.addEventListener(canHover ? "mouseenter" : "touchstart", () => {
// Get absolute position of the trigger element
const size = interestsElement.getBoundingClientRect();
explodeInterests(size.x, size.y);
});
// Explode interests when mouse hovers or touch hold starts
interestsElement.addEventListener(canHover ? "mouseenter" : "touchstart", (event) => explodeInterests(event.x, event.y));
// Implode interests when mouse leaves or touch hold ends
interestsElement.addEventListener(canHover ? "mouseleave" : "touchend", () => implodeInterests());
}

View file

@ -0,0 +1,4 @@
// Redirect to work page if no href is defined
if (!new URLSearchParams(window.location.search).has("href")) {
new vv.Navigation("/work").navigate();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View file

@ -49,20 +49,8 @@
<p>Can I put my own website here, is that cheating? Maybe, but I think this site counts as the most important thing I've personally created. I've only used my own libraries and frameworks to create this website, so it kind of works as a live demonstration of many of my web projects bundled together.</p>
<div class="actions">
<a href="https://codeberg.org/vlw/vlw.se"><button class="inline">
<p>read more</p>
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
</button></a>
</div>
</featured-item>
<featured-item>
<div class="title">
<?= VV::embed("public/assets/media/icons/vw.svg") ?>
</div>
<h3>Silly dabbles</h3>
<p>I create silly things for fun to challenge myself sometimes, and putting them all on the timeline is not right. So I made an appropriately-themed and named page to highlight most of my "what if I could" projects.</p>
<div class="actions">
<a href="/playground"><button class="inline">
<p>playground</p>
<?= VV::embed("public/assets/media/icons/codeberg.svg") ?>
<p>view source</p>
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
</button></a>
</div>
@ -83,35 +71,60 @@
</featured-item>
<featured-item>
<div class="title">
<h3>🍰</h3>
</div>
<h3>Still Alive</h3>
<p>I recreated the end credits from the video game Portal using pure JavaScript and browser windows. It was created using my old [abandoned] animation library and some patience. It's not perfect, it notably has a few time-drifting issues.</p>
<div class="actions">
<a href="https://blob.vlw.se/0195b948-8cd3-7d7f-8b21-e992a621a4c1.webm" target="_blank"><button class="inline">
<?= VV::embed("public/assets/media/icons/star.svg") ?>
<p>demo video</p>
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
</button></a>
<a href="https://codeberg.org/vlw/still-alive" target="_blank"><button class="inline collapse">
<p>view source</p>
<?= VV::embed("public/assets/media/icons/codeberg.svg") ?>
</button></a>
</div>
</featured-item>
</section>
<section class="heading">
<h1>web highligts</h1>
</section>
<section class="featured">
<featured-item>
<div class="title">
<a href="/work/archive?href=https://icellate.srv.vlw.se"><img src="/assets/media/img/preview-icellate.avif"></a>
</div>
<h3>Website for iCellate Medical</h3>
<p><?= (new Work("icellate/website"))->summary() ?></p>
<div class="actions">
<a href="/work/icellate/website"><button class="inline">
<p>read more</p>
<a href="/work/archive?href=https://icellate.srv.vlw.se"><button class="inline">
<?= VV::embed("public/assets/media/icons/star.svg") ?>
<p>preview</p>
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
</button></a>
</div>
</featured-item>
<featured-item>
<div class="title">
<?= VV::embed("public/assets/media/icons/star.svg") ?>
<a href="/work/archive?href=https://genemate.srv.vlw.se"><img src="/assets/media/img/preview-genemate.avif"></a>
</div>
<h3>Modernizing GeneMate by iCellate</h3>
<h3>Website for GeneMate by iCellate</h3>
<p><?= (new Work("icellate/genemate"))->summary() ?></p>
<div class="actions">
<a href="/work/icellate/genemate"><button class="inline">
<p>read more</p>
<a href="/work/archive?href=https://genemate.srv.vlw.se"><button class="inline">
<?= VV::embed("public/assets/media/icons/star.svg") ?>
<p>preview</p>
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
</button></a>
</div>
</featured-item>
<featured-item>
<div class="title">
<?= VV::embed("public/assets/media/icons/star.svg") ?>
<a href="/work/deltaco/asyncapp"><img src="/assets/media/img/preview-deltaco.avif"></a>
</div>
<h3>Custom pages for Deltaco AB</h3>
<h3>Campaign pages for Deltaco AB</h3>
<p><?= (new Work("deltaco/asyncapp"))->summary() ?></p>
<div class="actions">
<a href="/work/deltaco/asyncapp"><button class="inline">

12
public/work/archive.php Normal file
View file

@ -0,0 +1,12 @@
<style><?= VV::css("public/assets/css/pages/work/archive") ?></style>
<section>
<h1>This is an archived website!</h1>
<p>You're about to view an archived version of this website on my domain. Everything you see, and all features that are available on the archived website have been recreated to simulate the real behavior as closely as possible. Some features can unfortunately not be simulated properly and have been disabled completely. No actions you take on this website have any real effects.</p>
</section>
<section>
<a href="<?= $_GET["href"] ?? "" ?>" target="_blank"><button class="inline solid">
<p>Proceed to website</p>
<?= VV::embed("public/assets/media/icons/chevron.svg") ?>
</button></a>
</section>
<script><?= VV::js("public/assets/js/pages/work/archive") ?></script>

View file

@ -112,9 +112,9 @@
<p><?= $work->summary() ?></p>
<?php if ($work->actions()): ?>
<div class="actions">
<?php if ($work->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()): ?>
@ -130,14 +130,9 @@
<?php endif; ?>
</button></a>
<?php endforeach; ?>
<?php else: ?>
<a href="<?= "/work/{$work->id}" ?>"><button class="inline">
<p>read more</p>
<?= VV::embed(DEFAULT_BUTTON_ICON) ?>
</button></a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>

View file

@ -16,6 +16,8 @@
require_once VV::root("src/Database/Models/Work/Action.php");
class Work extends Model {
public const DATE_FORMAT = "Y-m-d";
public function __construct(public readonly string $id) {
parent::__construct(Endpoints::WORK, [
WorkTable::ID->value => $this->id
@ -34,8 +36,8 @@
return $this->get(WorkTable::SUMMARY->value);
}
public function created(): DateTimeImmutable {
return new DateTimeImmutable($this->get(WorkTable::CREATED->value));
public function created(): \DateTimeImmutable {
return new \DateTimeImmutable($this->get(WorkTable::CREATED->value));
}
public function tags(): array {

View file

@ -78,13 +78,16 @@ INSERT INTO `work` (`id`, `title`, `summary`, `created`) VALUES
('icellate/website', 'Website for iCellate Medical', 'Together with the iCellate team, I created a new front-end for the biopharma startup using my Vegvisir framework as the foundation.', '2023-04-19'),
('itg/lan', 'Reservation website for ITG-Sundbyberg', 'Redesign of IT-Gymnasiet Sundbyberg\'s seat reservation system, tournament registration, and information website for their yearly LAN events.', '2014-09-02'),
('itg/upload', 'Web project upload for ITG-Sundbyberg', 'Special school assignment for my Web programming course at IT-Gymnasiet Sundbyberg', '2014-06-11'),
('vlw/bbcb', 'Big Black Coffee Button', 'A very simple PWA for updating the \"coffe tally\" on my about page from anywhere in the world whenever I have a cup of coffee!', '2025-03-13'),
('vlw/camera-obscura', 'cameraobscura.gr', 'Portable front-end website for Camera Obscura GR', '2018-04-25'),
('vlw/collage', 'vlw/collage', 'Create an image where each \"pixel\" is a smaller image of similar color to the original image.', '2021-03-21'),
('vlw/curl', 'cURL wrapper Bash script', 'Public domain shell script optimized to be run in a Visual Studio Code-like interface that wraps cURL on any system with Bash installed. I created it to make manual requests for Reflect endpoints with API Bearer token keys.', '2025-02-09'),
('vlw/dediprison', 'DediPrison', 'Public Minecraft server project together with a friend that had around 20-30 active monthly players.', '2015-10-13'),
('vlw/disneyplus-pip', 'vlw/disneyplus-pip', 'Enable (or rather disable Disney\'s block of) picture-in-picture on disneyplus.com for Chrome.', '2021-01-31'),
('vlw/edkb', 'vlw/edkb', 'Printable keyboard overlay for some controls in Elite Dangerous.', '2021-03-18'),
('vlw/elevent', 'vlw/elevent', 'A small npm module that is intended to add more control over event listeners on HTMLElements with JavaScript. Kind of a superset of addEventListener.', '2024-11-11'),
('vlw/eyeart', 'eyeart.me', 'Website designed by me for the Greek/Swedish photographer, eyeart. The website features albums, a blog, and news pages.', '2014-03-02'),
('vlw/href', 'API-managed permalink redirector/URL shortener', 'This is a simple API-managed permalink generator/URL shortener that I created to hotlink resources for my projects. Permalink destinations can be altered if the target resource needs to be moved. Permalinks can also replace other permalinks with native inheritance at the database-level', '2025-02-09'),
('vlw/ion-musik', 'Website for ION Musik', 'Portable front-end website for Greek musican, ION Musik.', '2015-06-11'),
('vlw/labylib', 'LabyLib', 'Library for controlling LabyMod cosmetics programmatically in Python.', '2020-11-11'),
('vlw/labylib-animated-cape', 'vlw/labylib-animated-cape', 'Minecraft cosmetics scripts for my labylib library that cycles between a set of Labymod capes, creating a (slow) animation.', '2020-11-15'),
@ -133,7 +136,10 @@ INSERT INTO `work_actions` (`ref_work_id`, `icon_prepended`, `icon_appended`, `o
('vlw/php-sqlite', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/php-sqlite', NULL),
('vlw/php-functionflags', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/functionflags', NULL),
('vlw/still-alive', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/still-alive', NULL),
('vlw/still-alive', 'star.svg', NULL, 0, 'open demo', 'https://victorwesterlund.github.io/still-alive/', NULL);
('vlw/still-alive', 'star.svg', NULL, 0, 'open demo', 'https://victorwesterlund.github.io/still-alive/', NULL),
('vlw/bbcb', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/big-black-coffee-button', NULL),
('vlw/href', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/href', NULL),
('vlw/curl', 'codeberg.svg', NULL, 0, 'view source', 'https://codeberg.org/vlw/curl', NULL);
CREATE TABLE `work_tags` (
`ref_work_id` varchar(255) NOT NULL,
@ -196,7 +202,11 @@ INSERT INTO `work_tags` (`ref_work_id`, `label`) VALUES
('vlw/labylib-chattycape', 'VLW'),
('vlw/labylib-chattycape', 'REPO'),
('vlw/labylib-animated-cape', 'VLW'),
('vlw/labylib-animated-cape', 'REPO');
('vlw/labylib-animated-cape', 'REPO'),
('vlw/bbcb', 'VLW'),
('vlw/bbcb', 'WEBSITE'),
('vlw/href', 'VLW'),
('vlw/curl', 'VLW');
CREATE TABLE `work_timeline` (
`ref_work_id` varchar(255) NOT NULL,
@ -216,13 +226,16 @@ INSERT INTO `work_timeline` (`ref_work_id`, `year`, `month`, `day`) VALUES
('icellate/website', 2023, 4, 19),
('itg/lan', 2014, 9, 2),
('itg/upload', 2014, 6, 11),
('vlw/bbcb', 2025, 3, 13),
('vlw/camera-obscura', 2018, 4, 25),
('vlw/collage', 2021, 3, 21),
('vlw/curl', 2025, 2, 9),
('vlw/dediprison', 2015, 10, 13),
('vlw/disneyplus-pip', 2021, 1, 31),
('vlw/edkb', 2021, 3, 18),
('vlw/elevent', 2024, 11, 11),
('vlw/eyeart', 2014, 3, 2),
('vlw/href', 2025, 2, 9),
('vlw/ion-musik', 2015, 6, 11),
('vlw/labylib', 2020, 11, 11),
('vlw/labylib-animated-cape', 2020, 11, 15),