1
0
Fork 0

Add guides

This commit is contained in:
Owen Ryan 2024-02-05 23:13:57 -05:00
parent 5bf555aaa3
commit 65ec3ec18f
57 changed files with 510 additions and 91 deletions

View file

@ -33,3 +33,5 @@ source: source
collections: collections:
projects: projects:
output: true output: true
guides:
output: true

View file

@ -7,5 +7,9 @@ header:
link: /about link: /about
- name: Projects - name: Projects
link: /projects link: /projects
directory: true
- name: Guides
link: /guides
directory: true
- name: Contact - name: Contact
link: /contact link: /contact

View file

@ -17,10 +17,9 @@ links:
style: style:
fa-classes: [ fa-brands, fa-linkedin ] fa-classes: [ fa-brands, fa-linkedin ]
color-primary: 0077B5 color-primary: 0077B5
- name: LeetCode # No clue what to put here to I'm linking to my doom page lmao
url: https://leetcode.com/owenryan/ - name: DOOM
url: https://owenryan.us/doom
style: style:
# Leetcode's logo is not in fontawesome so just using this generic one fa-classes: [ fa-solid, fa-spaghetti-monster-flying ]
fa-classes: [ fa-solid, fa-laptop-code ] color-primary: A72626
# Leetcode does not have a press kit, and does not publish their brand colors. Hopefully this is close enough.
color-primary: FFA116

306
source/_guides/pfsense.md Normal file
View file

@ -0,0 +1,306 @@
---
layout: guide
title: "pfSense Guide"
description: How to convert an old desktop PC into a router/firewall combo
carousels:
installing:
title: Navigating the Install Wizard
steps:
- text: Wait for the installer to boot
image: /assets/images/guides/pfsense/setup-boot.png
- text: Accept the copyright agreement
image: /assets/images/guides/pfsense/setup-copyright.png
- text: Select `Install` on the welcome screen
image: /assets/images/guides/pfsense/setup-welcome.png
- text: For partitioning, select `Auto (ZFS)`
image: /assets/images/guides/pfsense/setup-partitioning.png
- text: Wait for the installer to probe for devices
image: /assets/images/guides/pfsense/setup-probe.png
- text: On the ZFS Configuration menu, select `Pool Type/Disks` and press enter
image: /assets/images/guides/pfsense/setup-zfs-menu-pool.png
- text: When asked for device type, choose `Striped`. Then press enter
image: /assets/images/guides/pfsense/setup-zfs-type.png
- text: Select your hard drive and press space to add it to the pool, then press enter
image: /assets/images/guides/pfsense/setup-zfs-drives.png
- text: You should now be back on the main ZFS configuration window. Select `Install` and press enter
image: /assets/images/guides/pfsense/setup-zfs-menu-install.png
- text: Confirm you would like to wipe the drive
image: /assets/images/guides/pfsense/setup-confirm.png
- text: Wait for it to install the Operating System
image: /assets/images/guides/pfsense/setup-installing.png
- text: When the installation completes, select `Restart`
image: /assets/images/guides/pfsense/setup-complete.png
- text: After the screen goes black, remove your USB drive
image: /assets/images/guides/pfsense/black.png
---
**NOTE: This article is still under development. Some sections are incomplete or need clarification**
# Goals / Who this is for
This guide is targeted at people who already understand the basics of computer networking (IP Addresses, DHCP, etc.),
and want to dive further down the discovery rabbithole, while also building a functioning firewall that can be
configured to help network security.
**Note:** pfSense requires a bit of time messing around in the web UI until you get the hang of it, so get ready to
spend
some time to get acquainted to the interface.
# Hardware
Pretty much any old desktop computer from the last ~15 years should be more than enough as long as it has:
- An x86-64 CPU (As long it's a consumer PC or server made in the last 15 years it should be fine)
- Two RJ45 Ethernet ports
- Most computers only have one, but one can be added through USB or PCIe
- I would recommend the [TP-Link TG-3468](https://www.tp-link.com/us/home-networking/pci-adapter/tg-3468/) as a
budget-friendly PCIe expansion card.
- At least 2GB of System Memory (RAM)
- pfSense requires 1GB, but more is needed when there are many connected devices.
- A USB drive to hold the installer (Must have > 1GB capacity)
pfSense is based on the FreeBSD operating system. A full list of supported hardware can be found
[on the FreeBSD website](https://www.freebsd.org/releases/13.0R/hardware/).
# Example network topology
In this guide, we will assume that this firewall is between your ISP-provided modem and your wireless router. If your
ISP only provided a modem/router combo box, contact support to ask about using your own router.
<img src="/assets/images/guides/pfsense/topology.png" alt="TODO">
# pfSense CE vs pfSense Plus
The pfSense branding applies to two different operating systems, pfSense CE (Community Edition) and pfSense Plus.
pfSense CE is Free and Open Source, meaning that the source code is freely available to view, modify, and redistribute.
pfSense Plus is a closed-source version maintained by Netgate.
For more information on the differences between pfSense CE and pfSense Plus, view the
[official FAQ](https://www.netgate.com/support/frequently-asked-questions-pfsense-plus)
**Note:** pfSense Plus used to be free for non-commercial use, but Netgate has removed that subscription tier. The
cheapest subscription plan is $129 per year.
# Installing the OS
Note: This guide assumes that your firewall has a video output. If you are using serial, please follow the
[official documentation](https://docs.netgate.com/pfsense/en/latest/install/download-installer-image.html)
## Downloading the OS
The pfSense CE download image can be obtained from [the pfSense website](https://www.pfsense.org/download/).
Architecture should be set to **AMD64**, and installer can be set to USB Memstick or DVD Image. Both can be flashed onto
a USB stick. [Here is a full comparison](https://en.wikipedia.org/wiki/IMG_(file_format)#Comparison_to_ISO_images)
If you have chosen to use pfSense plus, you can update from pfSense CE to pfSense Plus after installation.
## Flashing the Image onto a USB Stick
Once the file has downloaded, open the usb-flasher of your choice (I recommend
[Balena Etcher](https://etcher.balena.io/)), and flash the file.
**Note: This will wipe the contents of the USB stick, so make sure there's nothing important on it.**
# Booting into the installer
1. Insert the USB device into a USB port on the back of the motherboard (Front of the case should be fine but don't use
a USB hub)
2. Shut down the device
3. Start the device
4. Open the boot selection screen
- This is different for every computer, but normally holding down `DELETE` works.
- You can also try `F2` or `F12`
- If those don't work, consult your motherboard or computer's manual.
5. Select to boot from the USB drive
6. You should now see a pfSense boot screen. Press `Enter` or wait a few seconds for the installer to start booting
<img src="/assets/images/guides/pfsense/setup-bootloader.png" alt="TODO">
# Installing
{% include guide-carousel.html id="installing" %}
# Configuring pfSense
## Getting the router's IP address
After the system boot process completes, you should see a screen that looks like this.
<img src="/assets/images/guides/pfsense/cli-menu.png" alt="TODO">
The router displays its WAN address (public IP address), and LAN address (local IP address)
in my case, the local IP address is `192.168.1.1`
## Accessing the Web Dashboard
Going to `http://ROUTER_LOCAL_IP_ADDRESS` in your browser should bring you to a login screen
<img src="/assets/images/guides/pfsense/pfsense-login.png" alt="TODO">
The default username is `admin`, and the default password is `pfsense`
You should now be prompted with a setup wizard.
Skip step 1 since it's just an ad for paid support
<img src="/assets/images/guides/pfsense/wizard-welcome.png" alt="TODO">
On step 2:
- Set the hostname of the firewall (or leave it as pfSense)
- Give it a subdomain if you have one (TODO)
- Set DNS servers (1.1.1.1 and 1.0.0.1 are maintained by Cloudflare)
<img src="/assets/images/guides/pfsense/wizard-general.png" alt="TODO">
On step 3:
- Set the timezone
- Change the network timeserver if you are into that
<img src="/assets/images/guides/pfsense/wizard-timezone.png" alt="TODO">
On step 4, this is where the magic happens.
If this firewall is between your ISP provided modem, there is a good chance using DHCP will work completely fine, but it
depends on your specific ISP.
<img src="/assets/images/guides/pfsense/wizard-wan.png" alt="TODO">
On step 5, you can set your local IP Address range. The default is `192.168.1.1/16` but if you need more addresses, you
can use `10.0.0.0/8`
<img src="/assets/images/guides/pfsense/wizard-lan.png" alt="TODO">
On step 6, set the password for the webGUI. Make sure to use a secure password
<img src="/assets/images/guides/pfsense/wizard-password.png" alt="TODO">
On step 7, and 8: reload the system
The firewall will now restart, if you changed the IP address, you need to change the address in your browser
## Configuration
Note: This list is not comprehensive.
- Enable dark mode
- System > General Setup > webConfigurator section > Theme
- Set to pfSense-Dark
- Remove "Netgate Services and Support tab from dashboard"
- Press the X in the top-right corner of the window
- Add the traffic graph to the dashboard
- Press the plus in the top right corner of the dashboard and select "Traffic Graph"
- Upgrade to PFSense Plus if you are willing to pay at least $129/year
- Purchase a license from
the [PFSense Plus store page](https://shop.netgate.com/products/pfsense-software-subscription)
- Insert the code provided in System > Register
# Setting up IPv6
## What is IPv6
IPv6 is a newer implementation of the Internet Protocol than the classic IPv4 and was mainly created to add more IP
addresses. IPv4 supports a maximum of 2^32 (`4,294,967,296`) addresses, while IPv6 has a maximum of 2^128
(340,282,366,920,938,463,463,374,607,431,768,211,456) addresses.
## Do I need one?
IPv4 addresses are now a commodity, and thus some people (including me) have moved to hosting exclusively* using IPv6.
*My website is still accessible over the IPv4 internet through a network translation service, but I will probably pull
the plug when IPv6 becomes more widely adopted.
Your ISP might have already rolled out IPv6 to your area. You can check your IPv6 status using
[this website](https://test-ipv6.com/).
## Obtaining an IPv6 address block with TunnelBroker
Netgate provides an [official tutorial](https://docs.netgate.com/pfsense/en/latest/recipes/ipv6-tunnel-broker.html) on
how to get a block of IPv6 addresses with TunnelBroker and how to configure pfSense to use them.
# Installing and configuring Snort
Snort is a rule-based firewall software that blocks incoming and outgoing network packets based on user-configured
rules.
Another issue caused by the IPv4 address space being completely full is that people run bots that ping random addresses
for both research and malicious reasons.
## Installing the Snort package
1. Open the Package Manager's Available Package view (System > Package Manager > Available Packages)
<img src="/assets/images/guides/pfsense/pfsense-packages.png" alt="TODO">
2. Install the Snort Package
<img src="/assets/images/guides/pfsense/pfsense-package-confirm.png" alt="TODO">
3. Wait for the installation to complete
<img src="/assets/images/guides/pfsense/pfsense-package-install.png" alt="TODO">
## Configuring Snort
1. First, go to the snort configuration menu (Services > Snort), and click on the `Interfaces` tab if it does not send
you to that page.
<img src="/assets/images/guides/pfsense/snort-interfaces-empty.png" alt="TODO">
2. Add the WAN interface to be filtered by pressing the `Add` button in the bottom right
<img src="/assets/images/guides/pfsense/snort-interfaces-empty.png" alt="TODO">
3. Select the WAN interface
4. By default, snort will only save alerts of offences. For extra security, you can enable the `Block Offenders` option,
but ensure that it will only block the source address, and be aware that on rare occasion you might accidentally
block yourself.
5. Press save at the bottom of the page to save interface settings
6. Going back to the `Snort Interfaces` tab should show the WAN interface
<img src="/assets/images/guides/pfsense/snort-interfaces-off.png" alt="TODO">
7. Enable scanning by pressing the Play button and waiting for it to start
<img src="/assets/images/guides/pfsense/snort-interfaces-on.png" alt="TODO">
### Adding rules
Snort blocks connections based on rulesets that can be obtained from multiple sources:
- Snort VRT (Requires account, has free and paid tiers)
- Snort GPL (Free without account)
- ET Open (Free without account)
- ET Pro (Targeted at large companies; does not publicly list prices)
To Add rules:
1. Go to `Global Settings`
<img src="/assets/images/guides/pfsense/snort-globalconfig1.png" alt="TODO">
2. Enable the rule sources you desire
3. Set the update interval to 1 Day
<img src="/assets/images/guides/pfsense/snort-globalconfig2.png" alt="TODO">
4. Press `Save`
### Downloading Rules
Go to Updates and press the `Update Rules` button. This will fetch all rules you enabled in the previous step
### Configuring rules
1. Go to the `Snort Interfaces` tab
2. Click on the pencil icon associated with the WAN interface
3. Go to the `WAN Categories` tab
4. Choose the rule sets you would like to use
You can either cherry-pick which rules you would like to apply, or you can press `Select All` at the top, and manually
whitelist proper traffic that gets blocked.
### Viewing alerts in real time
In the `Alerts` tab, you can view IP addresses that have been blocked by the selected rules
<img src="/assets/images/guides/pfsense/snort-alerts.png" alt="TODO">
This image is from an actual pfSense deployment, so many fields have been blurred
# Conclusion
Congratulations! You now have an open-source* firewall protecting your network! Here are some links to other guides on
setting up specific packages/programs on pfSense:
- [Creating a Virtual Private Network with Tailscale](https://www.wundertech.net/how-to-set-up-tailscale-on-pfsense/)
- [Forwarding ports through your firewall](https://www.wundertech.net/pfsense-port-forwarding-setup-guide/)
- [Configuring VLANS, DHCP, and other stuff](https://itigic.com/how-to-configure-pfsense-internet-vlans-dhcp-dns-and-nat/)
*If you have chosen to stick with pfSense CE over pfSense+

View file

@ -0,0 +1,38 @@
{% assign carousel = page.carousels[include.id] %}
<div class="card">
<div id="guideCarousel-{{ include.id }}" class="carousel slide card-body">
<h2 class="card-title">{{ carousel.title }}</h2>
<div class="carousel-indicators">
{% for step in carousel.steps %}
<button type="button" data-bs-target="guideCarousel-{{ include.id }}" data-bs-slide-to="{{ forloop.index0 }}"
{% if forloop.index0 == 0 %}
class="active" aria-current="true"
{% endif %}
aria-label="Slide {{i}}">
</button>
{% endfor %}
</div>
<div class="carousel-inner" style="overflow: visible;">
{% for step in carousel.steps %}
<div class="carousel-item{% if forloop.index0 == 0 %} active{% endif %}">
<img src="{{ step.image }}" class="d-block w-100" alt="{{ step.image-alt }}">
<div class="carousel-caption d-none d-md-block text-body-emphasis"
style="background: rgba(75, 75, 75, .7);">
<h5>Step {{ forloop.index }}</h5>
<p>{{ step.text }}</p>
</div>
</div>
{% endfor %}
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#guideCarousel-{{ include.id }}" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#guideCarousel-{{ include.id }}" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div>
</div>

View file

@ -23,8 +23,10 @@
{% endcomment %} {% endcomment %}
<link href="/assets/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/assets/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet"> <link href="/css/style.css" rel="stylesheet">
<!-- Load fontawesome here for faster loadtimes: https://stackoverflow.com/a/35880730/9523246 --> {% comment %}
Load fontawesome here for faster loadtimes: https://stackoverflow.com/a/35880730/9523246
{% endcomment %}
<script> <script>
lazyLoadCSS('https://use.fontawesome.com/releases/v6.4.0/css/all.css'); lazyLoadCSS('https://use.fontawesome.com/releases/v6.4.2/css/all.css');
</script> </script>
</head> </head>

View file

@ -9,7 +9,7 @@
<ul class="nav nav-pills"> <ul class="nav nav-pills">
{%- for link in site.data.navigation.header -%} {%- for link in site.data.navigation.header -%}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{% if page.url == link.link %} active{% endif %}" href="{{ link.link }}" <a class="nav-link{% if page.url == link.link or link.directory and page.url contains link.link %} active{% endif %}" href="{{ link.link }}"
aria-current="page">{{ link.name }}</a> aria-current="page">{{ link.name }}</a>
</li> </li>
{%- endfor -%} {%- endfor -%}

View file

@ -0,0 +1,7 @@
---
layout: main
---
<div class="container">
{{ content }}
</div>

View file

@ -1,36 +0,0 @@
---
layout: project
title: "Informinator"
description: News aggregator built with Python, AioHTTP, and Jinja
thumbnail_url: /assets/images/informinator.webp
project_url: https://informinator.owenryan.us/
# TODO: Uncomment this when source code is published
# source_url: https://code.owenryan.us/owenryan/informinator
SEO_tags: [Python, aiohttp, News]
---
Informinator is the one-stop site for all of your news needs! It aims to be a central hub that delivers the stories you
need to know while also funneling you to the proper news sources to get more information. Informinator is currently in
a very early beta and only sources news from Al Jazeera, BBC News, The Associated Press, and editorials from The
Guardian.
**Note: Informinator will be open-sourced in the next few weeks when the server is stable**
## Origins
During the spring 2023 semester, two separate classes I was taking had weekly news quizzes that checked that you had
been following current events. I decided that the best way to study for these quizzes was to create a news aggregator
to simplify the process of looking at several news outlets per day.
## Current issues
The first version of Informinator was hacked together over a weekend, so some design choices are suboptimal. Some of these include:
- All articles are loaded on page-load, even though most are hidden behind the carousel.
- Informinator's RSS parsing code only supports feeds from [RSSHub](https://docs.rsshub.app/en/).
- RSSHub's New York Times English feed is currently unusable due to [a parsing error](https://github.com/DIYgod/RSSHub/issues/12371).
- All articles are cached in Python when an in-memory database such as Redis would greatly improve performance.
## Roadmap
Informinator's public roadmap can be found [here](https://informinator.owenryan.us/roadmap).

View file

@ -2,6 +2,8 @@
layout: project layout: project
title: Adventures with old calculators title: Adventures with old calculators
description: Shenanigans with graphing calculators my school was throwing away description: Shenanigans with graphing calculators my school was throwing away
year: current
permalink: /projects/calculators
thumbnail_url: /assets/images/calculators.webp thumbnail_url: /assets/images/calculators.webp
SEO_tags: [Calculator, Texas Instruments, TI82, Z80, Assembly, MS-DOS, Overclocking] SEO_tags: [Calculator, Texas Instruments, TI82, Z80, Assembly, MS-DOS, Overclocking]
--- ---

View file

@ -0,0 +1,53 @@
---
layout: project
title: "Informinator"
description: News aggregator built with React and TypeScript
year: current
permalink: /projects/informinator
thumbnail_url: /assets/images/informinator.webp
project_url: https://informinator.owenryan.us/
source_url: https://code.owenryan.us/owenryan/informinator
SEO_tags: [News, FOSS, React, RSS, TypeScript]
---
**Note: Informinator will be open-sourced in the near future when the site leaves the testing stage**
Informinator is a web-based news aggregator that acts as a central place to catch up on current events.
## Origins
During the spring 2023 semester, two separate classes I was taking had weekly news quizzes that checked that you had
been following current events. I decided that the best way to study for these quizzes was to create a news aggregator
to simplify the process of looking at several news outlets per day.
The original version of Informinator was hacked together in a weekend and worked much differently than the current
version. It was written entirely in Python using Aiohttp as the webserver and jinja as the template engine. The server
did everything from Parse RSS to render HTML, which led to 5+ second load times as every RSS feed had to be fetched
before HTML was returned. It also only pulled feeds from RSSHub, and thus only featured stories from BBC News,
Al jazeera, The Associated Press, and Op-eds from The Guardian.
## Privacy
Informinator aims to be a privacy-friendly solution for news aggregation. Some information is inadvertently collected to
prevent spam, but it can be entirely avoided by self-holding your own instance of Informinator.
The official Informinator instance collects the following information
- IP address when you load the page - This is logged by the webserver and used to identify Denial-Of-Service (DOS) and
similar attacks.
- IP address and RSS feed URL when using the official informinator proxy to load articles - Again used for detecting
abuse.
## Current issues
The first version of Informinator was hacked together over a weekend, so some design choices are suboptimal. Some of these include:
- All articles are loaded on page-load, even though most are hidden behind the carousel.
- Informinator's RSS parsing code only supports feeds from [RSSHub](https://docs.rsshub.app/en/).
- RSSHub's New York Times English feed is currently unusable due
to [a parsing error](https://github.com/DIYgod/RSSHub/issues/12371).
- All articles are cached in Python when an in-memory database such as Redis would greatly improve performance.
## Roadmap
Informinator's public roadmap can be found [here](https://informinator.owenryan.us/roadmap).

View file

@ -2,9 +2,11 @@
layout: project layout: project
title: "Jambox" title: "Jambox"
description: "Discord music bot built with Hikari and Lavalink" description: "Discord music bot built with Hikari and Lavalink"
year: current
permalink: /projects/jambox
project_url: https://discord.com/api/oauth2/authorize?client_id=1102705272805404722&permissions=4298394880&scope=applications.commands%20bot project_url: https://discord.com/api/oauth2/authorize?client_id=1102705272805404722&permissions=4298394880&scope=applications.commands%20bot
thumbnail_url: /assets/images/jambox.webp thumbnail_url: /assets/images/jambox.webp
SEO_tags: [Discord, Python, Hikari, Lavalink] SEO_tags: [ Discord, Python, Hikari, Lavalink ]
--- ---
**Note:** This project is currently on haitus due to a core Python library becoming deprecated. **Note:** This project is currently on haitus due to a core Python library becoming deprecated.

View file

@ -2,10 +2,12 @@
layout: project layout: project
title: This website title: This website
description: The self-built site using Jekyll, Bootstrap, TypeScript, and more! description: The self-built site using Jekyll, Bootstrap, TypeScript, and more!
year: current
permalink: /projects/website
thumbnail_url: /assets/images/website.webp thumbnail_url: /assets/images/website.webp
project_url: https://owenryan.us/ project_url: https://owenryan.us/
source_url: https://code.owenryan.us/owenryan/owenryan.us source_url: https://code.owenryan.us/owenryan/owenryan.us
SEO_tags: [web, html, css, jekyll] SEO_tags: [Website, HTML, CSS, Jekyll]
--- ---
After squatting on this domain for 7 years, I finally stopped procrastinating and decided to create a website. After squatting on this domain for 7 years, I finally stopped procrastinating and decided to create a website.
@ -14,8 +16,9 @@ Here is everything I used to turn my visions into reality.
## Structure ## Structure
This website was created using the [Jekyll](https://jekyllrb.com/) static site generator, which runs on the This website was created using the [Jekyll](https://jekyllrb.com/) static site generator, which runs on the
[Liquid](https://shopify.github.io/liquid/) template engine. Jekyll supports writing posts in [Markdown](https://en.wikipedia.org/wiki/Markdown), which makes writing simple pages [Liquid](https://shopify.github.io/liquid/) template engine. Jekyll supports writing posts in
like this one much easier. On the other hand, using plain markdown makes embedding videos and icons much more difficult. [Markdown](https://en.wikipedia.org/wiki/Markdown), which makes writing simple pages like this one much easier. On the
other hand, using plain markdown makes embedding videos and icons much more difficult.
## Theme ## Theme
@ -39,3 +42,11 @@ so compiling to JavaScript has to be done manually.
Jekyll produces static HTML and CSS files, meaning that they can be hosted anywhere. I chose to host this website using Jekyll produces static HTML and CSS files, meaning that they can be hosted anywhere. I chose to host this website using
the [Apache webserver](https://httpd.apache.org/) because it's reliable and has better documentation than NGINX. the [Apache webserver](https://httpd.apache.org/) because it's reliable and has better documentation than NGINX.
## DOOM Easter Egg
You might have noticed the spaghetti monster on the social media panel on the front page. I had no idea what to put
there, so I linked to a page with a built-in x86 emulator ([v86](https://github.com/copy/v86)), that runs a super
lightweight Linux image built with Buildroot that boots straight into the original DOOM via [Chocolate
Doom](https://www.chocolate-doom.org/wiki/index.php/Chocolate_Doom). [Feel free to check out the buildroot
configuration](https://code.owenryan.us/owenryan/buildroot-doom)

View file

@ -21,7 +21,7 @@
// Remove bullet points from the about page // Remove bullet points from the about page
// https://stackoverflow.com/questions/1027354/i-need-an-unordered-list-without-any-bullets // https://stackoverflow.com/questions/1027354/i-need-an-unordered-list-without-any-bullets
main li .remove-bullet-points li
list-style-type: none list-style-type: none
padding-left: 2em padding-left: 2em
text-indent: -2em text-indent: -1.25em

View file

@ -32,10 +32,12 @@ so [contact me](/contact) if you are interested!
Skills that I am still working on improving Skills that I am still working on improving
- <i class="fa-brands fa-docker"></i> Building Docker containers for Python projects - <i class="fa-brands fa-docker"></i> Building Docker containers for Python projects
- <i class="fa-solid fa-microchip"></i> C - <i class="fa-solid fa-microchip"></i> C/C++
- <i class="fa-solid fa-memory"></i> C++ - <i class="fa-brands fa-java"></i> Java
- <i class="fa-solid fa-book"></i> Don't worry, I read Design Patterns
- <i class="fa-solid fa-chart-line"></i> Metrics aggregation using Prometheus and Grafana - <i class="fa-solid fa-chart-line"></i> Metrics aggregation using Prometheus and Grafana
- <i class="fa-brands fa-square-js"></i> JavaScript/TypeScript - <i class="fa-brands fa-square-js"></i> JavaScript/TypeScript
- <i class="fa-brands fa-react"></i> Experience with React & Next.js
- <i class="fa-solid fa-pen-ruler"></i> Frontend programming & Design - <i class="fa-solid fa-pen-ruler"></i> Frontend programming & Design
- <i class="fa-brands fa-html5"></i> HTML - <i class="fa-brands fa-html5"></i> HTML
- <i class="fa-brands fa-css3-alt"></i> CSS/SASS - <i class="fa-brands fa-css3-alt"></i> CSS/SASS

View file

@ -1,14 +1,14 @@
// This code de-obfuscates my email and displays it on screen when the button is clicked // This code de-obfuscates my email and displays it on screen when the button is clicked
// It's not a perfect solution, but it should stop be enough to stop email scraping // It's not a perfect solution, but it should stop be enough to stop email scraping
// Email encoded in base64. This might be changed in the future to prevent scraping targeting base64 strings // Email encoded in base64. This might be changed in the future to prevent scraping targeting base64 strings
const email = "Y29udGFjdEBvd2Vucnlhbi51cwo="; var email = "Y29udGFjdEBvd2Vucnlhbi51cwo=";
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Get document elements // Get document elements
const button = document.querySelector("#emailButton"); var button = document.querySelector("#emailButton");
const emailDiv = document.querySelector("#email"); var emailDiv = document.querySelector("#email");
// Decode the email string and insert a mailto link into the DOM, then disable the button // Decode the email string and insert a mailto link into the DOM, then disable the button
button.addEventListener("click", function () { button.addEventListener("click", function () {
const decodedEmail = atob(email); var decodedEmail = atob(email);
emailDiv.insertAdjacentHTML("beforeend", "<div class=\"col\"><a href=\"mailto:".concat(decodedEmail, "\"><strong>").concat(decodedEmail, "</strong></a></div>")); emailDiv.insertAdjacentHTML("beforeend", "<div class=\"col\"><a href=\"mailto:".concat(decodedEmail, "\"><strong>").concat(decodedEmail, "</strong></a></div>"));
button.disabled = true; button.disabled = true;
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View file

@ -0,0 +1 @@
find "." -type f -name "*.png" -exec sh -c 'convert "$0" "${0%.png}.webp"' {} \;

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -1,11 +1,10 @@
// JS Code that rotates the gradient on the content box on the front page // JS Code that rotates the gradient on the content box on the front page
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const mainBox = document.querySelector("#rotating-gradient"); var mainBox = document.querySelector("#rotating-gradient");
let angle = 0; var angle = 0;
// Rotate the gradient 1 degree every 100ms // Rotate the gradient 1 degree every 100ms
setInterval(function () { setInterval(function () {
const gradient = "linear-gradient(".concat(angle, "deg, #1E1F46, #404040)"); mainBox.style.background = "linear-gradient(".concat(angle, "deg, #1E1F46, #404040)");
mainBox.style["background"] = gradient;
angle += 1; angle += 1;
if (angle >= 360) { if (angle >= 360) {
angle = 0; angle = 0;

View file

@ -6,8 +6,7 @@ document.addEventListener("DOMContentLoaded", (): void => {
// Rotate the gradient 1 degree every 100ms // Rotate the gradient 1 degree every 100ms
setInterval((): void => { setInterval((): void => {
const gradient: string = `linear-gradient(${angle}deg, #1E1F46, #404040)`; mainBox.style.background = `linear-gradient(${angle}deg, #1E1F46, #404040)`;
mainBox.style["background"] = gradient;
angle += 1; angle += 1;
if (angle >= 360) { if (angle >= 360) {
angle = 0 angle = 0

View file

@ -1,7 +1,7 @@
// Some useful functions used throughout my code // Some useful functions used throughout my code
// Based on this SO answer https://stackoverflow.com/a/35880730/9523246 // Based on this SO answer https://stackoverflow.com/a/35880730/9523246
function lazyLoadCSS(url) { function lazyLoadCSS(url) {
const css = document.createElement('link'); var css = document.createElement('link');
css.href = url; css.href = url;
css.rel = 'stylesheet'; css.rel = 'stylesheet';
css.type = 'text/css'; css.type = 'text/css';

View file

@ -11,6 +11,11 @@ permalink: /contact
<div id="email" class="row d-inline-flex"> <div id="email" class="row d-inline-flex">
<div class="col"> <div class="col">
<button id="emailButton" type="button" class="btn btn-primary">Get contact email</button> <button id="emailButton" type="button" class="btn btn-primary">Get contact email</button>
<noscript>
The "get email" button uses JavaScript to decode the email. I respect your choice to disable JavaScript.
My email is <strong>contact</strong> at this domain. (Sorry for the wording I really don't want it to be
scraped.
</noscript>
</div> </div>
</div> </div>
</div> </div>

22
source/guides.html Normal file
View file

@ -0,0 +1,22 @@
---
layout: main
permalink: /guides/index.html
---
<div class="container">
<div class="row row-cols-1 row-cols-md-2 g-4">
{%- for guide in site.guides -%}
<div class="col p-2">
<a class="card" href="{{ guide.url }}" style="text-decoration: none;">
{%- if guide.thumbnail_url -%}
<img src="{{ guide.thumbnail_url }}" class="card-img-top" alt="{{ guide.title }} thumbnail">
{%- endif -%}
<div class="card-body">
<h3 class="card-title text-body-emphasis">{{ guide.title }}</h3>
<p class="card-text">{{ guide.description }}</p>
</div>
</a>
</div>
{%- endfor -%}
</div>
</div>

View file

@ -3,7 +3,8 @@ layout: main
--- ---
<div class="container my-5"> <div class="container my-5">
<div id="rotating-gradient" class="col-7 p-5 text-center rounded-3 position-absolute top-50 start-50 translate-middle"> <div id="rotating-gradient"
class="col-7 p-5 text-center rounded-3 position-absolute top-50 start-50 translate-middle">
<h1 class="text-body-emphasis">Hello there!</h1> <h1 class="text-body-emphasis">Hello there!</h1>
<p class="col-lg-8 mx-auto fs-5 text-muted">Welcome to my website!</p> <p class="col-lg-8 mx-auto fs-5 text-muted">Welcome to my website!</p>

View file

@ -1,11 +1,11 @@
--- ---
layout: main layout: main
permalink: /projects permalink: /projects/index.html
--- ---
<div class="container"> <div class="container">
<div class="row"> <div class="row">
{%- for post in site.posts -%} {%- for post in site.projects -%}
<div class="col p-2"> <div class="col p-2">
<a class="card project-card" href="{{ post.url }}" style="text-decoration: None;"> <a class="card project-card" href="{{ post.url }}" style="text-decoration: None;">
{%- if post.thumbnail_url -%} {%- if post.thumbnail_url -%}