diff --git a/.gitignore b/.gitignore
index 70d179d..71b7a9a 100755
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,9 @@
-assets/media/content
+assets/js/modules/npm
# Bootstrapping #
#################
vendor
+node_modules
.env.ini
# OS generated files #
diff --git a/README.md b/README.md
index 1252ec5..cf40ab1 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,38 @@
# vlw.se
-This is the source code behind [vlw.se](https://vlw.se) which has been written from the ground up by me. This website is built on top of my [Vegvisir web framework](https://github.com/victorwesterlund/vegvisir) and my [Reflect API framework](https://github.com/victorwesterlund/reflect).
+This is the source code behind [vlw.se](https://vlw.se) which has been written from the ground up by me. This website is built on top of my [Vegvisir web framework](https://vegvisir.vlw.se) and my [Reflect API framework](https://reflect.vlw.se).
# Installation
If you for whatever reason want to get this website up and running for yourself this is how that is done.
-This website is built for PHP 8.0+ and MariaDB 14+ (for the API database).
+## This website requires the following prerequisites
+- [PHP 8.0+](https://www.php.net/)
+- [MariaDB 14+](https://mariadb.org/)
+- [The NPM package manager](https://www.npmjs.com/)
+- [The Reflect API framework](https://reflect.vlw.se)
+- [The Vegvisir web framework](https://vegvisir.vlw.se)
+- [The composer package manager](https://getcomposer.org/)
**Confimed supported framework versions:**
Vegvisir|Reflect
--|--
-✅ [`2.5.0`](https://github.com/VictorWesterlund/vegvisir/releases/tag/2.5.0)|✅ [`2.7.2`](https://github.com/VictorWesterlund/reflect/releases/tag/2.7.2)
+✅ [`3.0.1`](https://codeberg.org/vegvisir/vegvisir/releases/tag/3.0.1)|✅ [`2.7.2`](https://codeberg.org/reflect/reflect/releases/tag/2.7.2)
## Website (Vegvisir)
1. **Download this repo**
Git clone or download this repo to any local folder
```
- git clone https://github.com/VictorWesterlund/vlw.se
+ git clone https://codeberg.org/vlw/vlw.se
```
2. **Download and install Vegvisir**
- Follow the installation instructions for [Vegvisir](https://github.com/victorwesterlund/vegvisir) and point the `site_path` variable to the local vlw.se folder.
+ Follow the installation instructions for [Vegvisir](https://vegvisir.vlw.se/docs/installation) and point the `root_path` variable to your local vlw.se folder.
-3. **Install dependencies**
+3. **Run the install script**
- Install dependencies with composer.
+ This bash script will install dependencies and make npm modules public.
```
- composer install --optimize-autoloader
+ ./install.sh
```
Et voila! You probably want to install the API-side too but the website itself should now be accessible from your configured Vegvisir host.
@@ -40,16 +46,16 @@ The API (and database) is where most content is stored and served from on this w
Otherwise... Git clone or download this repo to any local folder
```
- git clone https://github.com/VictorWesterlund/vlw.se
+ git clone https://codeberg.org/vlw/vlw.se
```
2. **Download and install Reflect**
- Follow the installation instructions for [Reflect](https://github.com/victorwesterlund/vegvisir) and point the `endpoints` variable to the `/api` subdirectory in the local vlw.se folder.
+ Follow the installation instructions for [Reflect](https://reflect.vlw.se/docs/installation) and point the `endpoints` variable to the `/api` subdirectory in the local vlw.se folder.
3. **Install dependencies**
- Install dependencies with composer.
+ `cd` into the api folder and install dependencies with composer.
```
composer install --optimize-autoloader
```
@@ -62,9 +68,6 @@ The API (and database) is where most content is stored and served from on this w
Make a copy of `/api/.env.example.ini` and change the `[vlwdb]` variables with your MariaDB credentials.
- You also have to generate a [GitHub access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) if you wish to use the `releases` endpoint.
- [Read more about this endpoint here](#)
-
6. **Set environment variables for website**
It's reasonable to assume if you've installed the website from this repo that you'd also want to use the API with it. Start my making a copy of `/.env.example.ini` (root directory) and change the `[api]` variables to point to your API hostname.
diff --git a/api/install.sh b/api/install.sh
new file mode 100644
index 0000000..0c7900a
--- /dev/null
+++ b/api/install.sh
@@ -0,0 +1,2 @@
+# Install dependencies
+composer install --optimize-autoloader
\ No newline at end of file
diff --git a/assets/css/pages/about.css b/assets/css/pages/about.css
index 9bd3615..92739c8 100755
--- a/assets/css/pages/about.css
+++ b/assets/css/pages/about.css
@@ -5,7 +5,7 @@
--color-accent: rgb(var(--primer-color-accent));
}
-main {
+vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
@@ -15,7 +15,7 @@ main {
/* ## Divider */
-main > hr {
+vv-shell > hr {
border-color: rgba(255, 255, 255, .1);
}
diff --git a/assets/css/pages/about/battlestation-retired.css b/assets/css/pages/about/battlestation-retired.css
index c0cd09c..a6a92da 100644
--- a/assets/css/pages/about/battlestation-retired.css
+++ b/assets/css/pages/about/battlestation-retired.css
@@ -5,7 +5,7 @@
--color-accent: rgb(var(--primer-color-accent));
}
-main {
+vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
diff --git a/assets/css/pages/about/battlestation.css b/assets/css/pages/about/battlestation.css
index 84e4641..981c2cc 100644
--- a/assets/css/pages/about/battlestation.css
+++ b/assets/css/pages/about/battlestation.css
@@ -5,7 +5,7 @@
--color-accent: rgb(var(--primer-color-accent));
}
-main {
+vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
diff --git a/assets/css/pages/contact.css b/assets/css/pages/contact.css
index 9d1e4db..68c60e4 100755
--- a/assets/css/pages/contact.css
+++ b/assets/css/pages/contact.css
@@ -5,7 +5,7 @@
--color-accent: rgb(var(--primer-color-accent));
}
-main {
+vv-shell {
display: flex;
flex-direction: column;
align-items: center;
@@ -14,7 +14,7 @@ main {
/* # Sections */
-main > svg {
+vv-shell > svg {
margin: var(--padding) 0;
}
diff --git a/assets/css/pages/error.css b/assets/css/pages/error.css
index 5dd4d2a..c3beedb 100755
--- a/assets/css/pages/error.css
+++ b/assets/css/pages/error.css
@@ -6,7 +6,7 @@ header {
backdrop-filter: unset;
}
-main {
+vv-shell {
max-width: unset;
display: grid;
justify-items: center;
diff --git a/assets/css/pages/index.css b/assets/css/pages/index.css
index f1f6e6c..eb3f071 100755
--- a/assets/css/pages/index.css
+++ b/assets/css/pages/index.css
@@ -4,18 +4,18 @@ body[vv-top-page="/"]::before {
opacity: 0;
}
-/* # Main styles */
+/* # vv-shell styles */
/* ## Picture */
-main {
+vv-shell {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: column-reverse;
}
-main img {
+vv-shell img {
margin: auto;
width: 25vh;
pointer-events: none;
@@ -171,14 +171,14 @@ splash::after {
/* # Size quries */
@media (min-width: 900px) {
- main {
+ vv-shell {
display: grid;
grid-template-columns: repeat(2, 1fr);
justify-items: center;
align-items: center;
}
- main img {
+ vv-shell img {
width: 35vh;
}
}
diff --git a/assets/css/pages/search.css b/assets/css/pages/search.css
index 86fedee..ac9f75c 100755
--- a/assets/css/pages/search.css
+++ b/assets/css/pages/search.css
@@ -21,7 +21,7 @@ section.search {
margin-bottom: calc(var(--padding) * 2);
}
-main[vv-page="/search"] > section.search {
+vv-shell[vv-page="/search"] > section.search {
display: flex;
}
diff --git a/assets/css/pages/work.css b/assets/css/pages/work.css
index c39860c..829ae93 100755
--- a/assets/css/pages/work.css
+++ b/assets/css/pages/work.css
@@ -5,7 +5,7 @@
--color-accent: rgb(var(--primer-color-accent));
}
-main {
+vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
diff --git a/assets/css/document.css b/assets/css/shells/document.css
old mode 100755
new mode 100644
similarity index 98%
rename from assets/css/document.css
rename to assets/css/shells/document.css
index c30225d..f1e927b
--- a/assets/css/document.css
+++ b/assets/css/shells/document.css
@@ -260,24 +260,15 @@ header.searchboxActive searchbox {
transform: rotateX(0);
}
-/* ## Main */
+/* ## vv-shell */
-main {
+vv-shell {
position: relative;
padding: calc(var(--padding) * 1.5);
width: 100%;
max-width: 1000px;
}
-main > * {
- transition: 100ms opacity;
- opacity: 1;
-}
-
-main.loading > * {
- opacity: 0;
-}
-
/* ## Search results */
search-results {
diff --git a/assets/js/pages/about.js b/assets/js/pages/about.js
index be75c4f..bc933f2 100755
--- a/assets/js/pages/about.js
+++ b/assets/js/pages/about.js
@@ -1,5 +1,3 @@
-new vv.Interactions("about");
-
const randomIntFromInterval = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
}
diff --git a/assets/js/pages/about/battlestation-retired.js b/assets/js/pages/about/battlestation-retired.js
index 112c1d6..e69de29 100644
--- a/assets/js/pages/about/battlestation-retired.js
+++ b/assets/js/pages/about/battlestation-retired.js
@@ -1 +0,0 @@
-new vv.Interactions("battlestation-retired");
\ No newline at end of file
diff --git a/assets/js/pages/about/battlestation.js b/assets/js/pages/about/battlestation.js
index 4550346..d587920 100644
--- a/assets/js/pages/about/battlestation.js
+++ b/assets/js/pages/about/battlestation.js
@@ -1,19 +1,19 @@
-new vv.Interactions("battlestation", {
- toggleGroup: (event) => {
- // Collapse self if already active and current target
- if (event.target.classList.contains("active")) {
- return event.target.classList.remove("active");
- }
+import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
- // Collapse all and open current target
- [...event.target.closest(".specs").querySelectorAll(".group")].forEach(element => element.classList.remove("active"));
- event.target.classList.add("active");
- },
- setSpecActive: (event) => {
- event.target.classList.add("active");
-
- event.target.addEventListener("mouseleave", () => event.target.classList.remove("active"));
+new Elevent("click", document.querySelectorAll(".group"), (event) => {
+ // Collapse self if already active and current target
+ if (event.target.classList.contains("active")) {
+ return event.target.classList.remove("active");
}
+
+ // Collapse all and open current target
+ [...event.target.closest(".specs").querySelectorAll(".group")].forEach(element => element.classList.remove("active"));
+ event.target.classList.add("active");
+});
+
+new Elevent("click", document.querySelectorAll(".spec"), (event) => {
+ event.target.classList.add("active");
+ event.target.addEventListener("mouseleave", () => event.target.classList.remove("active"));
});
// Bind hover listeners for components in the SVGs
diff --git a/assets/js/pages/contact.js b/assets/js/pages/contact.js
index b98e377..e42bdab 100755
--- a/assets/js/pages/contact.js
+++ b/assets/js/pages/contact.js
@@ -10,8 +10,6 @@ class ContactForm {
[...document.querySelectorAll("form :is(input, textarea)")].forEach(element => {
element.addEventListener("keyup", () => this.saveMessage());
});
-
-
}
// Get saved message as JSON from SessionStorage
@@ -36,6 +34,7 @@ class ContactForm {
return ContactForm.removeSavedMessage();
}
+ // Set value of each input field in DOM by name attribute
for (const [name, value] of Object.entries(message)) {
this.form.querySelector(`[name="${name}"]`).value = value;
}
diff --git a/assets/js/pages/index.js b/assets/js/pages/index.js
index 0eeb43f..1fb023f 100755
--- a/assets/js/pages/index.js
+++ b/assets/js/pages/index.js
@@ -1,108 +1,108 @@
-const EMAIL_CPY_ANIM_DUR_MSECONDS = 1000;
+import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
-// Run email copied splash animation
-const emailCopiedAnimation = () => {
- const CONFETTI_COUNT = 40;
- const CONFETTI_SCALE_PIXELS = 300;
+// Click to copy email button
+{
+ const EMAIL_CPY_ANIM_DUR_MSECONDS = 1000;
- const randomIntFromInterval = (min, max) => {
- return Math.floor(Math.random() * (max - min + 1) + min)
+ // Run email copied splash animation
+ const emailCopiedAnimation = () => {
+ const CONFETTI_COUNT = 40;
+ const CONFETTI_SCALE_PIXELS = 300;
+
+ const randomIntFromInterval = (min, max) => {
+ return Math.floor(Math.random() * (max - min + 1) + min)
+ }
+
+ // Create new splash element
+ const splashElement = document.createElement("splash");
+ splashElement.innerText = "copied!";
+
+ // Set inline display to none to hide this element on pages where the splash element has no override styles defined.
+ splashElement.style.display = "none";
+
+ // Array of box-shadow strings as "confetti"
+ const confetti = [];
+
+ // Generate random confetti
+ for (let i = 0; i < CONFETTI_COUNT; i++) {
+ // Random confetti position
+ const x = randomIntFromInterval(CONFETTI_SCALE_PIXELS * -1, CONFETTI_SCALE_PIXELS);
+ const y = randomIntFromInterval(CONFETTI_SCALE_PIXELS * -1, CONFETTI_SCALE_PIXELS);
+
+ // Random confetti RGB color
+ const rgb = [
+ randomIntFromInterval(0, 255),
+ randomIntFromInterval(0, 255),
+ randomIntFromInterval(0, 255)
+ ];
+
+ // Interpolate random values and append to outer confetti array
+ confetti.push(`${x}px ${y}px 0 rgb(${rgb.join(",")})`);
+ }
+
+ // Set CSS variable on splash element that in turn will be used by pseudo-element
+ splashElement.style.setProperty("--confetti", confetti.join(","));
+
+ // Start animation by appending the created element to the document body
+ document.body.appendChild(splashElement);
+
+ // Run hide animation
+ setTimeout(() => {
+ splashElement.classList.add("hide");
+
+ // Selfdestruct element when hide animation finishes
+ setTimeout(() => splashElement.remove(), 400);
+ }, EMAIL_CPY_ANIM_DUR_MSECONDS + 100);
}
-
- // Create new splash element
- const splashElement = document.createElement("splash");
- splashElement.innerText = "copied!";
-
- // Set inline display to none to hide this element on pages where the splash element has no override styles defined.
- splashElement.style.display = "none";
-
- // Array of box-shadow strings as "confetti"
- const confetti = [];
-
- // Generate random confetti
- for (let i = 0; i < CONFETTI_COUNT; i++) {
- // Random confetti position
- const x = randomIntFromInterval(CONFETTI_SCALE_PIXELS * -1, CONFETTI_SCALE_PIXELS);
- const y = randomIntFromInterval(CONFETTI_SCALE_PIXELS * -1, CONFETTI_SCALE_PIXELS);
-
- // Random confetti RGB color
- const rgb = [
- randomIntFromInterval(0, 255),
- randomIntFromInterval(0, 255),
- randomIntFromInterval(0, 255)
- ];
-
- // Interpolate random values and append to outer confetti array
- confetti.push(`${x}px ${y}px 0 rgb(${rgb.join(",")})`);
- }
-
- // Set CSS variable on splash element that in turn will be used by pseudo-element
- splashElement.style.setProperty("--confetti", confetti.join(","));
-
- // Start animation by appending the created element to the document body
- document.body.appendChild(splashElement);
-
- // Run hide animation
- setTimeout(() => {
- splashElement.classList.add("hide");
-
- // Selfdestruct element when hide animation finishes
- setTimeout(() => splashElement.remove(), 400);
- }, EMAIL_CPY_ANIM_DUR_MSECONDS + 100);
-}
-
-new vv.Interactions("index", {
- // Copy email address to clipboard
- copyEmail: async () => {
+
+ new Elevent("click", document.querySelector(".email"), async () => {
try {
await navigator.clipboard.writeText("victor@vlw.se");
-
+
// Run "email copied" animation!
emailCopiedAnimation();
-
+
// NOTE: I don't know, spamming the button is kinda fun
// Prevent interactions with the copy email elements while the animation is running
/*[...document.querySelectorAll("[vv-call='copyEmail']")].forEach(element => {
//element.classList.add("lock");
-
+
setTimeout(() => element.classList.remove("lock"), EMAIL_CPY_ANIM_DUR_MSECONDS);
});*/
} catch (error) {
console.error(error.message);
}
- },
- // Open the fullscreen menu
- openMenu: () => document.querySelector("menu").classList.add("active"),
- // Close the fullscreen menu
- closeMenu: () => document.querySelector("menu").classList.remove("active")
-});
+ });
+}
// Change site accent color on hover of menu items
-if (window.matchMedia("(hover: hover)")) {
- // Update root CSS variables
- const updateColor = (rgb = null, hue = 0) => {
- if (!rgb) {
- document.documentElement.style.removeProperty("--hue-accent");
- document.documentElement.style.removeProperty("--primer-color-accent");
- document.documentElement.style.removeProperty("--color-accent");
+{
+ if (window.matchMedia("(hover: hover)")) {
+ // Update root CSS variables
+ const updateColor = (rgb = null, hue = 0) => {
+ if (!rgb) {
+ document.documentElement.style.removeProperty("--hue-accent");
+ document.documentElement.style.removeProperty("--primer-color-accent");
+ document.documentElement.style.removeProperty("--color-accent");
- return;
- }
+ return;
+ }
- document.documentElement.style.setProperty("--hue-accent", `${hue}deg`);
+ document.documentElement.style.setProperty("--hue-accent", `${hue}deg`);
- document.documentElement.style.setProperty("--primer-color-accent", `${rgb}`);
- // Compiled color variable must to be updated to receive the new RGB values
- document.documentElement.style.setProperty("--color-accent", "rgb(var(--primer-color-accent)");
- };
+ document.documentElement.style.setProperty("--primer-color-accent", `${rgb}`);
+ // Compiled color variable must to be updated to receive the new RGB values
+ document.documentElement.style.setProperty("--color-accent", "rgb(var(--primer-color-accent)");
+ };
- [...document.querySelectorAll("menu li")].forEach(element => {
- // Change site accent color to RGB and HUE rotation defined in element dataset
- element.addEventListener("mouseenter", (event) => updateColor(event.target.dataset.rgb, event.target.dataset.hue));
- // Reset initial accent color and hues
- element.addEventListener("mouseleave", () => updateColor());
- });
+ [...document.querySelectorAll("menu li")].forEach(element => {
+ // Change site accent color to RGB and HUE rotation defined in element dataset
+ element.addEventListener("mouseenter", (event) => updateColor(event.target.dataset.rgb, event.target.dataset.hue));
+ // Reset initial accent color and hues
+ element.addEventListener("mouseleave", () => updateColor());
+ });
- // Reset color on navigation
- document.querySelector(vv._env.MAIN).addEventListener(vv.Navigation.events.LOADED, () => updateColor(), { once: true });
-}
+ // Reset color on navigation
+ vv.Navigation.rootShellElement.addEventListener(vv.Navigation.EVENTS.STARTED, () => updateColor(), { once: true });
+ }
+}
\ No newline at end of file
diff --git a/assets/js/pages/search.js b/assets/js/pages/search.js
index 14f4494..e69de29 100755
--- a/assets/js/pages/search.js
+++ b/assets/js/pages/search.js
@@ -1 +0,0 @@
-new vv.Interactions("search");
\ No newline at end of file
diff --git a/assets/js/pages/work.js b/assets/js/pages/work.js
index 723281b..e69de29 100755
--- a/assets/js/pages/work.js
+++ b/assets/js/pages/work.js
@@ -1 +0,0 @@
-new vv.Interactions("work");
\ No newline at end of file
diff --git a/assets/js/document.js b/assets/js/shells/document.js
old mode 100755
new mode 100644
similarity index 58%
rename from assets/js/document.js
rename to assets/js/shells/document.js
index 829f2bb..95bc693
--- a/assets/js/document.js
+++ b/assets/js/shells/document.js
@@ -1,38 +1,37 @@
-new vv.Interactions("document", {
- navigateHome: () => new vv.Navigation("/").navigate(),
- closeSearchbox: () => {
+import { Elevent } from "/assets/js/modules/npm/Elevent.mjs";
+
+const CLASSNAME_SEARCHBOX_ACTIVE = "searchboxActive";
+
+// Handle search box open/close buttons
+{
+ // Open search box
+ new Elevent("click", document.querySelector(".searchbox-open"), () => {
+ document.querySelector("header").classList.add(CLASSNAME_SEARCHBOX_ACTIVE);
+ // Select searchbox inner input element
+ document.querySelector("searchbox input").focus();
+ });
+
+ // Close searchbox
+ new Elevent("click", document.querySelector(".searchbox-close"), () => {
// Disable search button interaction while animation is running
// This is required to prevent conflicts with the :hover "peak" transformation
const searchButtonElement = document.querySelector("header button.search");
const transformDuration = parseInt(window.getComputedStyle(searchButtonElement).getPropertyValue("--transform-duration"));
searchButtonElement.style.setProperty("pointer-events", "none");
- document.querySelector("header").classList.remove("searchboxActive");
+ document.querySelector("header").classList.remove(CLASSNAME_SEARCHBOX_ACTIVE);
// Wait for the transform animation to finish
setTimeout(() => searchButtonElement.style.removeProperty("pointer-events"), transformDuration);
- },
- openSearchbox: () => {
- document.querySelector("header").classList.add("searchboxActive");
- // Select searchbox inner input element
- document.querySelector("searchbox input").focus();
- }
-});
-
-// Crossfade pages on navigation
-{
- const mainElement = document.querySelector(vv._env.MAIN);
-
- mainElement.addEventListener(vv.Navigation.events.LOADING, () => {
- mainElement.classList.add("loading");
});
+}
- mainElement.addEventListener(vv.Navigation.events.LOADED, () => {
- // Close searchbox on main page navigation
- document.querySelector("header").classList.remove("searchboxActive");
-
- // Wait 200ms for the page fade-in animation to finish
- setTimeout(() => mainElement.classList.remove("loading"), 200);
+// Root shell navigation event handlers
+{
+ // On all top shell navigations
+ new Elevent(vv.Navigation.EVENTS.STARTED, vv.Navigation.rootShellElement, () => {
+ // Close searchbox on top shell navigations
+ document.querySelector("header").classList.remove(CLASSNAME_SEARCHBOX_ACTIVE);
});
}
diff --git a/install.sh b/install.sh
new file mode 100644
index 0000000..3e887ba
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,10 @@
+# Install dependencies
+composer install --optimize-autoloader
+npm install
+
+# (Re)create public NPM modules folder
+rm -r assets/js/modules/npm
+mkdir assets/js/modules/npm
+
+# Create link to Elevent MJS from public JS modules folder
+ln -sr node_modules/elevent/src/Elevent.mjs assets/js/modules/npm/Elevent.mjs
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..087a74c
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,17 @@
+{
+ "name": "vlw.se",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "elevent": "^1.0.2"
+ }
+ },
+ "node_modules/elevent": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/elevent/-/elevent-1.0.2.tgz",
+ "integrity": "sha512-ks5LBUBTg4Bpfmj99OcFAzuDGzBRDEZhTyxmq/Y3RbsdBQ4JCaIUYB0M15OBvBWgIn1BnCo4WCSmw0/YbCJliw=="
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..50060b0
--- /dev/null
+++ b/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "elevent": "^1.0.2"
+ }
+}
diff --git a/pages/error.php b/pages/error.php
deleted file mode 100755
index 623b01a..0000000
--- a/pages/error.php
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-404
-Hi, I'm
Victor Westerlund
@@ -31,7 +31,7 @@
website version: = VV::include("pages/about/version") ?>
+website version: = VV::include("public/about/version") ?>
I'd be happy to send you any component that you find here for "free". The only thing I ask in return is that you pay for shipping.
@@ -33,4 +33,4 @@Motherboard
Case
CPU
GPU
PSU
DRAM
- = VV::media("icons/chevron.svg") ?> + = VV::embed("assets/media/icons/chevron.svg") ?>DRAM - = $dram[DramModel::TECHNOLOGY->value] ?>
Storage
- = VV::media("icons/chevron.svg") ?> + = VV::embed("assets/media/icons/chevron.svg") ?>= $storage[StorageModel::DISK_FORMFACTOR->value] ?> = $storage[StorageModel::DISK_TYPE->value] ?>
The best way to get in touch is by email, or with the form on this page. I will try to reply as quickly as possible, probably within a few hours. The time is = (new DateTime("now", new DateTimeZone($_ENV["time"]["date_time_zone"])))->format("h:i a") ?> in Sweden right now.
my key is also listed on the openPGP key server for victor@vlw.se so your e-mail client can automatically retreive it if supported.
Start typing to search
Most of my free open-source software is available on GitHub and it's also mirrored on my server