lavender.software webring #2
16 changed files with 589 additions and 569 deletions
|
@ -1,12 +1,12 @@
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
end_of_line = crlf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.rs]
|
[*.rs]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
||||||
/target
|
/target
|
||||||
/dist
|
/dist
|
||||||
|
|
76
LICENSE.md
76
LICENSE.md
|
@ -1,38 +1,38 @@
|
||||||
The Charlotte Public License version 0.1
|
The Charlotte Public License version 0.1
|
||||||
|
|
||||||
Copyright 2021, Lavender Software (collectively, the "Author" henceforth).
|
Copyright 2021, Lavender Software (collectively, the "Author" henceforth).
|
||||||
|
|
||||||
This license gives everyone permission to examine, modify, and use this software
|
This license gives everyone permission to examine, modify, and use this software
|
||||||
and the associated documentation (the "Inator"), without patent obstacles, while protecting
|
and the associated documentation (the "Inator"), without patent obstacles, while protecting
|
||||||
the Author and any contributors (the "Composers") from liability.
|
the Author and any contributors (the "Composers") from liability.
|
||||||
|
|
||||||
Each Composer permits you to examine, modify, utilize, and distribute the Inator
|
Each Composer permits you to examine, modify, utilize, and distribute the Inator
|
||||||
where it would otherwise infringe upon that Composer's copyright or any patent claims that
|
where it would otherwise infringe upon that Composer's copyright or any patent claims that
|
||||||
they hold.
|
they hold.
|
||||||
|
|
||||||
No contributor may revoke this license, but the Author may choose to release the Inator
|
No contributor may revoke this license, but the Author may choose to release the Inator
|
||||||
(including the contributed works of any other Composer) under a different license.
|
(including the contributed works of any other Composer) under a different license.
|
||||||
|
|
||||||
You may not use the Inator to accrue revenue without explicit permission from the Author.
|
You may not use the Inator to accrue revenue without explicit permission from the Author.
|
||||||
|
|
||||||
You may not use the Inator to do Malevolence. If you are
|
You may not use the Inator to do Malevolence. If you are
|
||||||
notified that you have committed a Malevolence instrumented by the Inator, your license is
|
notified that you have committed a Malevolence instrumented by the Inator, your license is
|
||||||
terminated unless you take all practical steps to comply within a reasonable timeframe.
|
terminated unless you take all practical steps to comply within a reasonable timeframe.
|
||||||
|
|
||||||
The definition of Malevolence is at the discretion of the Author. It may include, but is not
|
The definition of Malevolence is at the discretion of the Author. It may include, but is not
|
||||||
limited to:
|
limited to:
|
||||||
|
|
||||||
- The promotion of bigotry, including: sexism, transphobia, homophobia, ableism, or the
|
- The promotion of bigotry, including: sexism, transphobia, homophobia, ableism, or the
|
||||||
perpetuation of racial oppression.
|
perpetuation of racial oppression.
|
||||||
- Causing a detriment to public health.
|
- Causing a detriment to public health.
|
||||||
- Instigating political, economic, or corporeal violence.
|
- Instigating political, economic, or corporeal violence.
|
||||||
- Entrenching an empire.
|
- Entrenching an empire.
|
||||||
- Where applicable, use of the Inator without the informed consent of a second party who may
|
- Where applicable, use of the Inator without the informed consent of a second party who may
|
||||||
object to its use.
|
object to its use.
|
||||||
|
|
||||||
The Inator is provided without any warranty, "as-is". No Composer is liable for any damages
|
The Inator is provided without any warranty, "as-is". No Composer is liable for any damages
|
||||||
related to the Inator.
|
related to the Inator.
|
||||||
|
|
||||||
In order to receive this license, you must agree to the terms set out in this document.
|
In order to receive this license, you must agree to the terms set out in this document.
|
||||||
This license, authorial attribution, and copyright notice must be distributed with
|
This license, authorial attribution, and copyright notice must be distributed with
|
||||||
any copies or large portions of the Inator.
|
any copies or large portions of the Inator.
|
||||||
|
|
49
README.md
49
README.md
|
@ -1,17 +1,32 @@
|
||||||
# lavender.software
|
# lavender.software
|
||||||
|
|
||||||
Static site generated using [siru](https://github.com/videogame-hacker/siru) and hosted at [lavender.software](https://lavender.software/).
|
Static site generated using [siru](https://github.com/videogame-hacker/siru) and hosted at [lavender.software](https://lavender.software/).
|
||||||
|
|
||||||
## Deploying
|
## Setting Up
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ # To set up, ensure that the 'dist' folder reflects the VPS
|
$ git clone 'git@lavender.software:lavender/lavender.software.git'
|
||||||
$ git clone 'root@lavender.software:/srv/http/lavender.software' dist
|
$ cd lavender.software/
|
||||||
$
|
lavender.software/ $ mkdir dist # or follow instructions in 'Deploying'
|
||||||
$ cargo run # Build the site
|
lavender.software/ $ cargo run
|
||||||
$ cd dist/
|
...
|
||||||
dist/ $ git add -A . && git commit -m "Deploying: $(date)"
|
lavender.software/ $ # Built files are in dist/
|
||||||
dist/ $ git pull --rebase
|
```
|
||||||
dist/ $ git push
|
|
||||||
dist/ $ # Your changes should now be live at lavender.software
|
You may want to `cd dist && python -m http.server` to get a local HTTP server.
|
||||||
```
|
|
||||||
|
## Deploying
|
||||||
|
|
||||||
|
**Note:** You don't need to do this unless you're th eone deploying the site to the production environment.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ # To set up, ensure that the 'dist' folder reflects the VPS
|
||||||
|
$ git clone 'root@lavender.software:/srv/http/lavender.software' dist
|
||||||
|
$
|
||||||
|
$ cargo run # Build the site
|
||||||
|
$ cd dist/
|
||||||
|
dist/ $ git add -A . && git commit -m "Deploying: $(date)"
|
||||||
|
dist/ $ git pull --rebase
|
||||||
|
dist/ $ git push
|
||||||
|
dist/ $ # Your changes should now be live at lavender.software
|
||||||
|
```
|
||||||
|
|
6
build.rs
6
build.rs
|
@ -1,3 +1,3 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("cargo:rerun-if-changed=build_src");
|
println!("cargo:rerun-if-changed=build_src");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub fn copy_assets(ctx: &BuildContext) -> Result<()> {
|
pub fn copy_assets(ctx: &BuildContext) -> Result<()> {
|
||||||
log_info("Copying assets…");
|
log_info("Copying assets…");
|
||||||
copy_dir_recursive(ctx.source_dir.join("assets"), ctx.output_dir.join("assets"))?;
|
copy_dir_recursive(ctx.source_dir.join("assets"), ctx.output_dir.join("assets"))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,84 +1,84 @@
|
||||||
use siru::prelude::*;
|
use siru::prelude::*;
|
||||||
use std::{convert::TryInto, fs, path::Path, path::PathBuf, sync::Arc, time::Duration};
|
use std::{convert::TryInto, fs, path::Path, path::PathBuf, sync::Arc, time::Duration};
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
mod assets;
|
mod assets;
|
||||||
mod main_page;
|
mod main_page;
|
||||||
mod webring;
|
mod webring;
|
||||||
|
|
||||||
pub struct BuildContext {
|
pub struct BuildContext {
|
||||||
source_dir: PathBuf,
|
source_dir: PathBuf,
|
||||||
output_dir: PathBuf,
|
output_dir: PathBuf,
|
||||||
write_pipeline: WritePipeline,
|
write_pipeline: WritePipeline,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SiruFS for BuildContext {
|
impl SiruFS for BuildContext {
|
||||||
fn get_source_dir(&self) -> &PathBuf {
|
fn get_source_dir(&self) -> &PathBuf {
|
||||||
&self.source_dir
|
&self.source_dir
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_output_dir(&self) -> &PathBuf {
|
fn get_output_dir(&self) -> &PathBuf {
|
||||||
&self.output_dir
|
&self.output_dir
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_write_pipeline(&self) -> &WritePipeline {
|
fn get_write_pipeline(&self) -> &WritePipeline {
|
||||||
&self.write_pipeline
|
&self.write_pipeline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy_dir_recursive(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
|
pub fn copy_dir_recursive(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
|
||||||
fs::create_dir_all(&dst)?;
|
fs::create_dir_all(&dst)?;
|
||||||
for entry in fs::read_dir(src)? {
|
for entry in fs::read_dir(src)? {
|
||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
let ty = entry.file_type()?;
|
let ty = entry.file_type()?;
|
||||||
if ty.is_dir() {
|
if ty.is_dir() {
|
||||||
copy_dir_recursive(entry.path(), dst.as_ref().join(entry.file_name()))?;
|
copy_dir_recursive(entry.path(), dst.as_ref().join(entry.file_name()))?;
|
||||||
} else {
|
} else {
|
||||||
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
|
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build() {
|
fn build() {
|
||||||
let ctx = BuildContext {
|
let ctx = BuildContext {
|
||||||
source_dir: "src".try_into().unwrap(),
|
source_dir: "src".try_into().unwrap(),
|
||||||
output_dir: "dist".try_into().unwrap(),
|
output_dir: "dist".try_into().unwrap(),
|
||||||
write_pipeline: WritePipeline::new(),
|
write_pipeline: WritePipeline::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let ctx = Arc::new(ctx);
|
let ctx = Arc::new(ctx);
|
||||||
|
|
||||||
[
|
[
|
||||||
main_page::main_page,
|
main_page::main_page,
|
||||||
assets::copy_assets,
|
assets::copy_assets,
|
||||||
webring::copy_webring,
|
webring::copy_webring,
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|f| {
|
.map(|f| {
|
||||||
let ctx = Arc::clone(&ctx);
|
let ctx = Arc::clone(&ctx);
|
||||||
std::thread::spawn(move || f(&ctx).unwrap())
|
std::thread::spawn(move || f(&ctx).unwrap())
|
||||||
})
|
})
|
||||||
.for_each(|t| t.join().unwrap());
|
.for_each(|t| t.join().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
build();
|
build();
|
||||||
|
|
||||||
if matches!(std::env::args().nth(1).as_deref(), Some("watch")) {
|
if matches!(std::env::args().nth(1).as_deref(), Some("watch")) {
|
||||||
use notify::*;
|
use notify::*;
|
||||||
|
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
let mut watcher = watcher(tx, Duration::from_millis(100)).unwrap();
|
let mut watcher = watcher(tx, Duration::from_millis(100)).unwrap();
|
||||||
watcher.watch("./src", RecursiveMode::Recursive).unwrap();
|
watcher.watch("./src", RecursiveMode::Recursive).unwrap();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match rx.recv() {
|
match rx.recv() {
|
||||||
Ok(_) => build(),
|
Ok(_) => build(),
|
||||||
Err(_) => break,
|
Err(_) => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,58 +1,58 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
struct Member {
|
struct Member {
|
||||||
name: String,
|
name: String,
|
||||||
website: String,
|
website: String,
|
||||||
title: String,
|
title: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&(&str, &str, &str)> for Member {
|
impl From<&(&str, &str, &str)> for Member {
|
||||||
fn from(tuple: &(&str, &str, &str)) -> Self {
|
fn from(tuple: &(&str, &str, &str)) -> Self {
|
||||||
Member {
|
Member {
|
||||||
name: tuple.0.to_string(),
|
name: tuple.0.to_string(),
|
||||||
website: tuple.1.to_string(),
|
website: tuple.1.to_string(),
|
||||||
title: tuple.2.to_string(),
|
title: tuple.2.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "main_page.html.j2")]
|
#[template(path = "main_page.html.j2")]
|
||||||
struct MainPageTemplate {
|
struct MainPageTemplate {
|
||||||
members: Vec<Member>,
|
members: Vec<Member>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_page(ctx: &BuildContext) -> Result<()> {
|
pub fn main_page(ctx: &BuildContext) -> Result<()> {
|
||||||
log_info("Rendering main page…");
|
log_info("Rendering main page…");
|
||||||
|
|
||||||
let members = [
|
let members = [
|
||||||
("charlotte som", "https://som.codes/", "founder"),
|
("charlotte som", "https://som.codes/", "founder"),
|
||||||
("agatha rose", "https://agatharose.dev/", "meow"),
|
("agatha rose", "https://agatharose.dev/", "meow"),
|
||||||
("maya", "https://1312.gay/", "chief director of maya"),
|
("maya", "https://1312.gay/", "chief director of maya"),
|
||||||
(
|
(
|
||||||
"Luna Lulu",
|
"Luna Lulu",
|
||||||
"https://lunaisa.dev",
|
"https://lunaisa.dev",
|
||||||
"critically acclaimed website maker",
|
"critically acclaimed website maker",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"annie versario",
|
"annie versario",
|
||||||
"https://annie.kitty.lgbt",
|
"https://annie.kitty.lgbt",
|
||||||
"regional marquee technician",
|
"regional marquee technician",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"The System",
|
"The System",
|
||||||
"https://the-system.eu.org",
|
"https://the-system.eu.org",
|
||||||
"lead systems specialist",
|
"lead systems specialist",
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
ctx.write(
|
ctx.write(
|
||||||
"index.html",
|
"index.html",
|
||||||
MainPageTemplate {
|
MainPageTemplate {
|
||||||
members: members.iter().map(|x| x.into()).collect(),
|
members: members.iter().map(|x| x.into()).collect(),
|
||||||
}
|
}
|
||||||
.render()?,
|
.render()?,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub fn copy_webring(ctx: &BuildContext) -> Result<()> {
|
pub fn copy_webring(ctx: &BuildContext) -> Result<()> {
|
||||||
log_info("Copying webring…");
|
log_info("Copying webring…");
|
||||||
copy_dir_recursive(
|
copy_dir_recursive(
|
||||||
ctx.source_dir.join("webring"),
|
ctx.source_dir.join("webring"),
|
||||||
ctx.output_dir.join("webring"),
|
ctx.output_dir.join("webring"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
/* confettijs.org MIT */ const Confetti = (() => { "use strict"; const e = 10; let t, o, n = 75, i = 25, r = 1, s = !1, l = !0, a = [], d = (new Date).getTime(); function p(e) { if (t = document.createElement("canvas"), o = t.getContext("2d"), t.width = 2 * window.innerWidth, t.height = 2 * window.innerHeight, t.style.position = "fixed", t.style.top = 0, t.style.left = 0, t.style.width = "calc(100%)", t.style.height = "calc(100%)", t.style.margin = 0, t.style.padding = 0, t.style.zIndex = 999999999, t.style.pointerEvents = "none", document.body.appendChild(t), null != e) { let t = document.getElementById(e); null != t && t.addEventListener("click", e => { !function (e, t) { let o = []; for (let i = 0; i < n; i++)o.push(c(e, t)); a.push({ particles: o }) }(2 * e.clientX, 2 * e.clientY), l && (e.target.style.visibility = "hidden") }) } window.addEventListener("resize", () => { t.width = 2 * window.innerWidth, t.height = 2 * window.innerHeight }) } function y(e) { return e.pos.y - 2 * e.size.x > 2 * window.innerHeight } function c(e, t) { let o = (16 * Math.random() + 4) * r, n = (4 * Math.random() + 4) * r; return { pos: { x: e - o / 2, y: t - n / 2 }, vel: h(), size: { x: o, y: n }, rotation: 360 * Math.random(), rotation_speed: 10 * (Math.random() - .5), hue: 360 * Math.random(), opacity: 100, lifetime: Math.random() + .25 } } function h() { let e = Math.random() - .5, t = Math.random() - .7, o = Math.sqrt(e * e + t * t); return t /= o, { x: (e /= o) * (Math.random() * i), y: t * (Math.random() * i) } } return p.prototype.setCount = (e => { "number" == typeof e ? n = e : console.error("[ERROR] Confetti.setCount() - Input needs to be of type 'number'") }), p.prototype.setPower = (e => { "number" == typeof e ? i = e : console.error("[ERROR] Confetti.setPower() - Input needs to be of type 'number'") }), p.prototype.setSize = (e => { "number" == typeof e ? r = e : console.error("[ERROR] Confetti.setSize() - Input needs to be of type 'number'") }), p.prototype.setFade = (e => { "boolean" == typeof e ? s = e : console.error("[ERROR] Confetti.setFade() - Input needs to be of type 'boolean'") }), p.prototype.destroyTarget = (e => { "boolean" == typeof e ? l = e : console.error("[ERROR] Confetti.destroyTarget() - Input needs to be of type 'boolean'") }), window.requestAnimationFrame(function t(n) { let i = (n - d) / 1e3; d = n; for (let t = a.length - 1; t >= 0; t--) { let o = a[t]; for (let t = o.particles.length - 1; t >= 0; t--) { let n = o.particles[t]; n.vel.y += e * (n.size.y / (10 * r)) * i, n.vel.x += 25 * (Math.random() - .5) * i, n.vel.x *= .98, n.vel.y *= .98, n.pos.x += n.vel.x, n.pos.y += n.vel.y, n.rotation += n.rotation_speed, s && (n.opacity -= n.lifetime), y(n) && o.particles.splice(t, 1) } 0 == o.particles.length && a.splice(t, 1) } !function () { o.clearRect(0, 0, 2 * window.innerWidth, 2 * window.innerHeight); for (const d of a) for (const a of d.particles) e = a.pos.x, t = a.pos.y, n = a.size.x, i = a.size.y, r = a.rotation, s = a.hue, l = a.opacity, o.save(), o.beginPath(), o.translate(e + n / 2, t + i / 2), o.rotate(r * Math.PI / 180), o.rect(-n / 2, -i / 2, n, i), o.fillStyle = `hsla(275deg, ${s}%, ${s / 3.6}%, ${l}%)`, o.fill(), o.restore(); var e, t, n, i, r, s, l }(), window.requestAnimationFrame(t) }), p })();
|
/* confettijs.org MIT */ const Confetti = (() => { "use strict"; const e = 10; let t, o, n = 75, i = 25, r = 1, s = !1, l = !0, a = [], d = (new Date).getTime(); function p(e) { if (t = document.createElement("canvas"), o = t.getContext("2d"), t.width = 2 * window.innerWidth, t.height = 2 * window.innerHeight, t.style.position = "fixed", t.style.top = 0, t.style.left = 0, t.style.width = "calc(100%)", t.style.height = "calc(100%)", t.style.margin = 0, t.style.padding = 0, t.style.zIndex = 999999999, t.style.pointerEvents = "none", document.body.appendChild(t), null != e) { let t = document.getElementById(e); null != t && t.addEventListener("click", e => { !function (e, t) { let o = []; for (let i = 0; i < n; i++)o.push(c(e, t)); a.push({ particles: o }) }(2 * e.clientX, 2 * e.clientY), l && (e.target.style.visibility = "hidden") }) } window.addEventListener("resize", () => { t.width = 2 * window.innerWidth, t.height = 2 * window.innerHeight }) } function y(e) { return e.pos.y - 2 * e.size.x > 2 * window.innerHeight } function c(e, t) { let o = (16 * Math.random() + 4) * r, n = (4 * Math.random() + 4) * r; return { pos: { x: e - o / 2, y: t - n / 2 }, vel: h(), size: { x: o, y: n }, rotation: 360 * Math.random(), rotation_speed: 10 * (Math.random() - .5), hue: 360 * Math.random(), opacity: 100, lifetime: Math.random() + .25 } } function h() { let e = Math.random() - .5, t = Math.random() - .7, o = Math.sqrt(e * e + t * t); return t /= o, { x: (e /= o) * (Math.random() * i), y: t * (Math.random() * i) } } return p.prototype.setCount = (e => { "number" == typeof e ? n = e : console.error("[ERROR] Confetti.setCount() - Input needs to be of type 'number'") }), p.prototype.setPower = (e => { "number" == typeof e ? i = e : console.error("[ERROR] Confetti.setPower() - Input needs to be of type 'number'") }), p.prototype.setSize = (e => { "number" == typeof e ? r = e : console.error("[ERROR] Confetti.setSize() - Input needs to be of type 'number'") }), p.prototype.setFade = (e => { "boolean" == typeof e ? s = e : console.error("[ERROR] Confetti.setFade() - Input needs to be of type 'boolean'") }), p.prototype.destroyTarget = (e => { "boolean" == typeof e ? l = e : console.error("[ERROR] Confetti.destroyTarget() - Input needs to be of type 'boolean'") }), window.requestAnimationFrame(function t(n) { let i = (n - d) / 1e3; d = n; for (let t = a.length - 1; t >= 0; t--) { let o = a[t]; for (let t = o.particles.length - 1; t >= 0; t--) { let n = o.particles[t]; n.vel.y += e * (n.size.y / (10 * r)) * i, n.vel.x += 25 * (Math.random() - .5) * i, n.vel.x *= .98, n.vel.y *= .98, n.pos.x += n.vel.x, n.pos.y += n.vel.y, n.rotation += n.rotation_speed, s && (n.opacity -= n.lifetime), y(n) && o.particles.splice(t, 1) } 0 == o.particles.length && a.splice(t, 1) } !function () { o.clearRect(0, 0, 2 * window.innerWidth, 2 * window.innerHeight); for (const d of a) for (const a of d.particles) e = a.pos.x, t = a.pos.y, n = a.size.x, i = a.size.y, r = a.rotation, s = a.hue, l = a.opacity, o.save(), o.beginPath(), o.translate(e + n / 2, t + i / 2), o.rotate(r * Math.PI / 180), o.rect(-n / 2, -i / 2, n, i), o.fillStyle = `hsla(275deg, ${s}%, ${s / 3.6}%, ${l}%)`, o.fill(), o.restore(); var e, t, n, i, r, s, l }(), window.requestAnimationFrame(t) }), p })();
|
||||||
const c = new Confetti("purple");
|
const c = new Confetti("purple");
|
||||||
c.destroyTarget(false);
|
c.destroyTarget(false);
|
||||||
|
|
|
@ -1,88 +1,88 @@
|
||||||
:root {
|
:root {
|
||||||
--bg: rgb(28, 23, 36);
|
--bg: rgb(28, 23, 36);
|
||||||
--fg: rgb(234, 234, 248);
|
--fg: rgb(234, 234, 248);
|
||||||
--accent: hsl(275, 57%, 68%);
|
--accent: hsl(275, 57%, 68%);
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
font-size: 1.125em;
|
font-size: 1.125em;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-bottom: 1px solid var(--accent);
|
border-bottom: 1px solid var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
header,
|
header,
|
||||||
main,
|
main,
|
||||||
footer {
|
footer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 90ch;
|
max-width: 90ch;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
header img {
|
header img {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 2em;
|
margin-right: 2em;
|
||||||
height: 8em;
|
height: 8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#purple {
|
#purple {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
color: var(--bg);
|
color: var(--bg);
|
||||||
background-color: var(--accent);
|
background-color: var(--accent);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
line-height: 1.6em;
|
line-height: 1.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer nav ul {
|
footer nav ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer nav ul > li {
|
footer nav ul > li {
|
||||||
display: inline;
|
display: inline;
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
padding-bottom: 0.15em;
|
padding-bottom: 0.15em;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer nav ul > li + li {
|
footer nav ul > li + li {
|
||||||
border-inline-start: 1px solid var(--fg);
|
border-inline-start: 1px solid var(--fg);
|
||||||
|
|
||||||
padding-inline-start: 1ch;
|
padding-inline-start: 1ch;
|
||||||
margin-inline-start: 1ch;
|
margin-inline-start: 1ch;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "annie",
|
"id": "annie",
|
||||||
"name": "annieversary",
|
"name": "annieversary",
|
||||||
"url": "https://versary.town/"
|
"url": "https://versary.town/"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "charlotte",
|
"id": "charlotte",
|
||||||
"name": "charlotte",
|
"name": "charlotte",
|
||||||
"url": "https://char.lt/"
|
"url": "https://char.lt/"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "mira",
|
"id": "mira",
|
||||||
"name": "mira",
|
"name": "mira",
|
||||||
"url": "https://boxin.space/"
|
"url": "https://boxin.space/"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
162
src/webring/webring-0.1.0.js
Normal file
162
src/webring/webring-0.1.0.js
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
(function () {
|
||||||
|
function getUID() {
|
||||||
|
var array = new Uint8Array(8);
|
||||||
|
window.crypto.getRandomValues(array);
|
||||||
|
return Array.from(array)
|
||||||
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = document.currentScript.dataset;
|
||||||
|
const UID = getUID();
|
||||||
|
|
||||||
|
const css = `
|
||||||
|
#--lavender-cssreset-${UID} {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
#--lavender-webring-container-${UID} {
|
||||||
|
background-color: rgb(28, 23, 36);
|
||||||
|
background-blend-mode: multiply;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
background-color: ${params.backgroundColor || "purple"};
|
||||||
|
color: ${params.textColor || "white"};
|
||||||
|
text-shadow: 0px 1px 1px ${params.textShadowColor || "black"};
|
||||||
|
padding: 1px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
|
||||||
|
margin: 10px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#--lavender-webring-container-${UID} a {
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#--lavender-webring-container-${UID}::before {
|
||||||
|
display: block;
|
||||||
|
content: ' ';
|
||||||
|
top: 0; left: 0; bottom: 0; right: 0;
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 5px outset;
|
||||||
|
opacity: 0.25;
|
||||||
|
border-color: white black black white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#--lavender-webring-container-${UID} #--lavender-webring-title-${UID} {
|
||||||
|
margin: 10px 10px 0px;
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
#--lavender-webring-container-${UID} #--lavender-webring-item-container-${UID} {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
#--lavender-webring-container-${UID} #--lavender-webring-item-container-${UID} {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#--lavender-webring-container-${UID} .--lavender-webring-items-${UID} {
|
||||||
|
font-size: 14px;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 10px 10px 8px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#--lavender-webring-container-${UID} #--lavender-webring-item-1-${UID}::before {
|
||||||
|
display: inline;
|
||||||
|
content: '\\2190\\00a0';
|
||||||
|
}
|
||||||
|
#--lavender-webring-container-${UID} #--lavender-webring-item-3-${UID}::after {
|
||||||
|
display: inline;
|
||||||
|
content: '\\00a0\\2192';
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const webring_content = `
|
||||||
|
<div id="--lavender-cssreset-${UID}">
|
||||||
|
<div id="--lavender-webring-container-${UID}">
|
||||||
|
<p id="--lavender-webring-title-${UID}">This website is a part of the Lavender Software webring</p>
|
||||||
|
<div id="--lavender-webring-item-container-${UID}">
|
||||||
|
<a class="--lavender-webring-items-${UID}" id="--lavender-webring-item-1-${UID}"></a>
|
||||||
|
<a class="--lavender-webring-items-${UID}" id="--lavender-webring-item-2-${UID}"></a>
|
||||||
|
<a class="--lavender-webring-items-${UID}" id="--lavender-webring-item-3-${UID}"></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function renderIdx(data, element, idx) {
|
||||||
|
idx = (idx + data.length) % data.length;
|
||||||
|
|
||||||
|
datum = data[idx];
|
||||||
|
element.textContent = datum.name;
|
||||||
|
element.href = datum.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getElementByPostfixedId(path) {
|
||||||
|
return document.getElementById(path + "-" + UID);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderContent(currentScript, data) {
|
||||||
|
const params = currentScript.dataset;
|
||||||
|
|
||||||
|
var headstyle = document.createElement("style");
|
||||||
|
headstyle.innerHTML = css;
|
||||||
|
document.head.appendChild(headstyle);
|
||||||
|
|
||||||
|
currentScript.insertAdjacentHTML("afterend", webring_content);
|
||||||
|
|
||||||
|
const container = getElementByPostfixedId("--lavender-webring-container");
|
||||||
|
|
||||||
|
const item1 = getElementByPostfixedId("--lavender-webring-item-1");
|
||||||
|
const item2 = getElementByPostfixedId("--lavender-webring-item-2");
|
||||||
|
const item3 = getElementByPostfixedId("--lavender-webring-item-3");
|
||||||
|
|
||||||
|
const id = params.siteId;
|
||||||
|
var idindex = -1;
|
||||||
|
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
if (data[i].id == id) {
|
||||||
|
idindex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idindex == -1) {
|
||||||
|
item2.textContent =
|
||||||
|
"this site was not found in the list. please check that you don't have any typos in the id!";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("hideTitle" in params) {
|
||||||
|
getElementByPostfixedId("--lavender-webring-title").style.display =
|
||||||
|
"none";
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIdx(data, item1, idindex - 1);
|
||||||
|
renderIdx(data, item2, idindex);
|
||||||
|
renderIdx(data, item3, idindex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentScript = document.currentScript;
|
||||||
|
if (currentScript) {
|
||||||
|
fetch("https://lavender.software/webring/data.json")
|
||||||
|
.then(function (response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function (data) {
|
||||||
|
renderContent(currentScript, data);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("cannot locate document.currentScript element. aborting...");
|
||||||
|
}
|
||||||
|
})();
|
|
@ -1,157 +0,0 @@
|
||||||
(function() {
|
|
||||||
function getUID() {
|
|
||||||
var array = new Uint8Array(8);
|
|
||||||
window.crypto.getRandomValues(array);
|
|
||||||
return Array.from(array).map(b => b.toString(16).padStart(2, "0")).join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = document.currentScript.dataset;
|
|
||||||
const UID = getUID();
|
|
||||||
|
|
||||||
const css = `
|
|
||||||
#--lavender-cssreset-${UID} {
|
|
||||||
all: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
#--lavender-webring-container-${UID} {
|
|
||||||
background-color: rgb(28, 23, 36);
|
|
||||||
background-blend-mode: multiply;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
background-color: ${params.backgroundColor || "purple"};
|
|
||||||
color: ${params.textColor || "white"};
|
|
||||||
text-shadow: 0px 1px 1px ${params.textShadowColor || "black"};
|
|
||||||
padding: 1px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-family: sans-serif;
|
|
||||||
|
|
||||||
margin: 10px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#--lavender-webring-container-${UID} a {
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#--lavender-webring-container-${UID}::before {
|
|
||||||
display: block;
|
|
||||||
content: ' ';
|
|
||||||
top: 0; left: 0; bottom: 0; right: 0;
|
|
||||||
position: absolute;
|
|
||||||
box-sizing: border-box;
|
|
||||||
pointer-events: none;
|
|
||||||
border: 5px outset;
|
|
||||||
opacity: 0.25;
|
|
||||||
border-color: white black black white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#--lavender-webring-container-${UID} #--lavender-webring-title-${UID} {
|
|
||||||
margin: 10px 10px 0px;
|
|
||||||
text-align: center;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
#--lavender-webring-container-${UID} #--lavender-webring-item-container-${UID} {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
#--lavender-webring-container-${UID} #--lavender-webring-item-container-${UID} {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#--lavender-webring-container-${UID} .--lavender-webring-items-${UID} {
|
|
||||||
font-size: 14px;
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 10px 10px 8px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#--lavender-webring-container-${UID} #--lavender-webring-item-1-${UID}::before {
|
|
||||||
display: inline;
|
|
||||||
content: '\\2190\\00a0';
|
|
||||||
}
|
|
||||||
#--lavender-webring-container-${UID} #--lavender-webring-item-3-${UID}::after {
|
|
||||||
display: inline;
|
|
||||||
content: '\\00a0\\2192';
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const webring_content = `
|
|
||||||
<div id="--lavender-cssreset-${UID}">
|
|
||||||
<div id="--lavender-webring-container-${UID}">
|
|
||||||
<p id="--lavender-webring-title-${UID}">This website is a part of the Lavender Software webring</p>
|
|
||||||
<div id="--lavender-webring-item-container-${UID}">
|
|
||||||
<a class="--lavender-webring-items-${UID}" id="--lavender-webring-item-1-${UID}"></a>
|
|
||||||
<a class="--lavender-webring-items-${UID}" id="--lavender-webring-item-2-${UID}"></a>
|
|
||||||
<a class="--lavender-webring-items-${UID}" id="--lavender-webring-item-3-${UID}"></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function renderIdx(data, element, idx) {
|
|
||||||
idx = (idx + data.length) % data.length;
|
|
||||||
|
|
||||||
datum = data[idx];
|
|
||||||
element.textContent = datum.name;
|
|
||||||
element.href = datum.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getElementByPostfixedId(path) {
|
|
||||||
return document.getElementById(path + "-" + UID);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderContent(currentScript, data) {
|
|
||||||
const params = currentScript.dataset;
|
|
||||||
|
|
||||||
var headstyle = document.createElement('style');
|
|
||||||
headstyle.innerHTML = css;
|
|
||||||
document.head.appendChild(headstyle);
|
|
||||||
|
|
||||||
currentScript.insertAdjacentHTML("afterend", webring_content);
|
|
||||||
|
|
||||||
const container = getElementByPostfixedId('--lavender-webring-container');
|
|
||||||
|
|
||||||
const item1 = getElementByPostfixedId('--lavender-webring-item-1');
|
|
||||||
const item2 = getElementByPostfixedId('--lavender-webring-item-2');
|
|
||||||
const item3 = getElementByPostfixedId('--lavender-webring-item-3');
|
|
||||||
|
|
||||||
const id = params.siteId;
|
|
||||||
var idindex = -1;
|
|
||||||
|
|
||||||
for (var i = 0; i < data.length; i++) {
|
|
||||||
if (data[i].id == id) {
|
|
||||||
idindex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (idindex == -1) {
|
|
||||||
item2.textContent = "this site was not found in the list. please check that you don't have any typos in the id!";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("hideTitle" in params) {
|
|
||||||
getElementByPostfixedId('--lavender-webring-title').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIdx(data, item1, idindex - 1);
|
|
||||||
renderIdx(data, item2, idindex);
|
|
||||||
renderIdx(data, item3, idindex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentScript = document.currentScript;
|
|
||||||
if (currentScript) {
|
|
||||||
fetch("https://lavender.software/webring/data.json").then(function(response) {
|
|
||||||
return response.json();
|
|
||||||
}).then(function(data) {
|
|
||||||
renderContent(currentScript, data);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log("cannot locate document.currentScript element. aborting...");
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
|
@ -1,9 +1,9 @@
|
||||||
<footer>
|
<footer>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong class="brand">lavender software ltd</strong></li>
|
<li><strong class="brand">lavender software ltd</strong></li>
|
||||||
<li><a href="https://git.lavender.software/lavender/lavender.software">source code</a></li>
|
<li><a href="https://git.lavender.software/lavender/lavender.software">source code</a></li>
|
||||||
<!-- <li><a href="/about/">about us</a></li> -->
|
<!-- <li><a href="/about/">about us</a></li> -->
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -1,62 +1,62 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<title>lavender software | digital product studio</title>
|
<title>lavender software | digital product studio</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/styles.css">
|
<link rel="stylesheet" href="/assets/styles.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<img alt="Lavender Logo" src="/assets/logo.svg">
|
<img alt="Lavender Logo" src="/assets/logo.svg">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1>lavender software</h1>
|
<h1>lavender software</h1>
|
||||||
<p>the kind of software that we make is … <em id="purple">purple</em></p>
|
<p>the kind of software that we make is … <em id="purple">purple</em></p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<section>
|
<section>
|
||||||
<h2>projects</h2>
|
<h2>projects</h2>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="https://git.lain.faith/videogame-hacker/discord-css-injector">lavender cord</a> - a theming platform for Discord</li>
|
<li><a href="https://git.lain.faith/videogame-hacker/discord-css-injector">lavender cord</a> - a theming platform for Discord</li>
|
||||||
<li>more soon!</li>
|
<li>more soon!</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<section>
|
<section>
|
||||||
<h2>who are we?</h2>
|
<h2>who are we?</h2>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for member in members %}
|
{% for member in members %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ member.website }}">{{ member.name }}</a> - {{ member.title }}
|
<a href="{{ member.website }}">{{ member.name }}</a> - {{ member.title }}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</section> -->
|
</section> -->
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>services</h2>
|
<h2>services</h2>
|
||||||
|
|
||||||
<p>we offer:</p>
|
<p>we offer:</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>individual software consulting</li>
|
<li>individual software consulting</li>
|
||||||
<li>system operations for-hire</li>
|
<li>system operations for-hire</li>
|
||||||
<li>contractual project work</li>
|
<li>contractual project work</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{% include "_footer.html.j2" %}
|
{% include "_footer.html.j2" %}
|
||||||
|
|
||||||
<script async src="/assets/main_page/confetti.js"></script>
|
<script async src="/assets/main_page/confetti.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue