Format code

Used `deno fmt`.
This commit is contained in:
Foster Hangdaan 2024-03-05 18:38:42 -05:00
parent dd69d2a761
commit 5b2d1d93e4
Signed by: foster
GPG key ID: E48D7F49A852F112
16 changed files with 501 additions and 229 deletions

View file

@ -2,13 +2,21 @@ interface Props {
post: any;
}
export default function(props: Props) {
export default function (props: Props) {
return (
<li className="post-list-item">
<a href={props.post.data.url} className="post-list-title">{props.post.data.title}</a>
<time className="post-list-date">{Intl.DateTimeFormat("en-CA", { dateStyle: "long" }).format(props.post.data.date)}</time>
<a href={props.post.data.url} className="post-list-title">
{props.post.data.title}
</a>
<time className="post-list-date">
{Intl.DateTimeFormat("en-CA", { dateStyle: "long" }).format(
props.post.data.date,
)}
</time>
<ul className="tag-list">
{props.post.data.tags.map((tag,index) => <li key={index} className="tag">{tag}</li>)}
{props.post.data.tags.map((tag, index) => (
<li key={index} className="tag">{tag}</li>
))}
</ul>
<p className="post-list-description">{props.post.data.description}</p>
</li>

View file

@ -1,4 +1,4 @@
export default function({ comp }) {
export default function ({ comp }) {
const iconStyle = {
filter: "var(--filter-fg)",
};
@ -31,7 +31,10 @@ export default function({ comp }) {
alt="rss"
/>
</a>
<a href="https://www.fosterhangdaan.com/blog/feed.json" title="JSON Feed">
<a
href="https://www.fosterhangdaan.com/blog/feed.json"
title="JSON Feed"
>
<img
src="https://static.fosterhangdaan.com/icons/tabler-icons/latest/svg/braces.svg"
className="icon"
@ -41,7 +44,18 @@ export default function({ comp }) {
</a>
</div>
<p>Copyright &copy; 2023 Foster Hangdaan</p>
<p>Made with <a href="https://lume.land/">Lume</a>, <a href="https://www.typescriptlang.org/">TypeScript</a> and lots of <img src="https://static.fosterhangdaan.com/icons/tabler-icons/latest/svg/heart.svg" className="icon" style={{ filter: "var(--filter-red)" }} title="love" alt="love"/>.</p>
<p>
Made with <a href="https://lume.land/">Lume</a>,{" "}
<a href="https://www.typescriptlang.org/">TypeScript</a> and lots of
{" "}
<img
src="https://static.fosterhangdaan.com/icons/tabler-icons/latest/svg/heart.svg"
className="icon"
style={{ filter: "var(--filter-red)" }}
title="love"
alt="love"
/>.
</p>
</footer>
);
}

View file

@ -8,18 +8,21 @@ export interface Props {
comp: any;
}
export default function(props: Props) {
const dateFormatted = Intl.DateTimeFormat("en-CA", { dateStyle: "long" }).format(props.date);
export default function (props: Props) {
const dateFormatted = Intl.DateTimeFormat("en-CA", { dateStyle: "long" })
.format(props.date);
return (
<header className="page-header">
<h1>{ props.title }</h1>
<h1>{props.title}</h1>
{props.author &&
<p className="author" style={{ color: "var(--color-brown)" }}>
By {props.author.name} on <time dateTime={props.date.toISOString()}>{dateFormatted}</time>
</p>
}
<p className="subheading">{ props.description }</p>
(
<p className="author" style={{ color: "var(--color-brown)" }}>
By {props.author.name} on{" "}
<time dateTime={props.date.toISOString()}>{dateFormatted}</time>
</p>
)}
<p className="subheading">{props.description}</p>
{props.comp.separator()}
</header>
)
);
}

View file

@ -1,4 +1,4 @@
export default function() {
export default function () {
const iconStyle = {
filter: "var(--filter-green)",
};

View file

@ -1,7 +1,7 @@
export default function () {
return (
<div className="separator">
<span className="outer-outline"/>
<span className="outer-outline" />
<span>
<img
src="https://static.fosterhangdaan.com/icons/tabler-icons/latest/svg/diamonds.svg"
@ -10,7 +10,7 @@ export default function () {
alt=""
/>
</span>
<span className="outer-outline"/>
<span className="outer-outline" />
</div>
);
}

View file

@ -44,14 +44,14 @@ site.use(feed({
description: "=description",
published: "=date",
updated: "=updated",
}
},
}));
site.use(code_highlight({
languages: {
typescript: lang_typescript,
javascript: lang_javascript,
bash: lang_bash,
}
},
}));
site.use(toc({
slugify: {
@ -66,13 +66,15 @@ site.process([".html"], (pages) => {
// NOTE: This is a hack to append a class to JS doctrings so that we
// can style them. If only the Hightlight.js plugin could be configured
// to do this instead.
page.document?.getElementsByClassName("hljs-comment").forEach((codeCommentElement) => {
const docStringRegex = /^\/\*\*.*\*\/$/gsm;
const matchResult = codeCommentElement.innerText.match(docStringRegex);
if (matchResult) {
codeCommentElement.classList.add("docstring");
}
});
page.document?.getElementsByClassName("hljs-comment").forEach(
(codeCommentElement) => {
const docStringRegex = /^\/\*\*.*\*\/$/gsm;
const matchResult = codeCommentElement.innerText.match(docStringRegex);
if (matchResult) {
codeCommentElement.classList.add("docstring");
}
},
);
});
});

View file

@ -1,33 +1,76 @@
export default function({ title, description, children, comp, metas, links, author, date }) {
export default function (
{ title, description, children, comp, metas, links, author, date },
) {
return (
<html lang="en-CA">
<head>
<title>{title}</title>
<meta charSet="utf-8"/>
<meta name="description" content={description}/>
<meta name="author" content="Foster Hangdaan"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="color-scheme" content="dark light"/>
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1a1b26"/>
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#d5d6db"/>
<meta charSet="utf-8" />
<meta name="description" content={description} />
<meta name="author" content="Foster Hangdaan" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark light" />
<meta
name="theme-color"
media="(prefers-color-scheme: dark)"
content="#1a1b26"
/>
<meta
name="theme-color"
media="(prefers-color-scheme: light)"
content="#d5d6db"
/>
{Array.isArray(metas) && metas.length > 0 &&
metas.map((m,index) => <meta key={index} name={m.name} content={m.content}/>)
}
<link rel="stylesheet" href="/styles/main.css"/>
<link rel="icon" type="image/png" href="/icons/tabicon-16.png" sizes="16x16" />
<link rel="icon" type="image/png" href="/icons/tabicon-32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/icons/tabicon-96.png" sizes="96x96" />
<link rel="icon" type="image/png" href="/icons/tabicon-128.png" sizes="128x128" />
<link rel="icon" type="image/png" href="/icons/tabicon-196.png" sizes="196x196" />
metas.map((m, index) => (
<meta key={index} name={m.name} content={m.content} />
))}
<link rel="stylesheet" href="/styles/main.css" />
<link
rel="icon"
type="image/png"
href="/icons/tabicon-16.png"
sizes="16x16"
/>
<link
rel="icon"
type="image/png"
href="/icons/tabicon-32.png"
sizes="32x32"
/>
<link
rel="icon"
type="image/png"
href="/icons/tabicon-96.png"
sizes="96x96"
/>
<link
rel="icon"
type="image/png"
href="/icons/tabicon-128.png"
sizes="128x128"
/>
<link
rel="icon"
type="image/png"
href="/icons/tabicon-196.png"
sizes="196x196"
/>
{Array.isArray(links) && links.length > 0 &&
links.map((l,index) => <link key={index} rel={l.rel} href={l.href} type={l.type} title={l.title}/>)
}
links.map((l, index) => (
<link
key={index}
rel={l.rel}
href={l.href}
type={l.type}
title={l.title}
/>
))}
</head>
<body>
{ comp.navbar() }
{ comp.header({title, description, author, date}) }
{comp.navbar()}
{comp.header({ title, description, author, date })}
<main className="main-content">{children}</main>
{ comp.footer() }
{comp.footer()}
</body>
</html>
);

View file

@ -1,42 +1,50 @@
export const layout = "./base.tsx";
export default function({ children, toc, footnotes }) {
export default function ({ children, toc, footnotes }) {
return (
<>
{ toc.length > 0 &&
<nav className="toc">
<h2>Table of Contents</h2>
<ol>
{toc.map((item,index) => (
<li key={index}>
<a href={`#${item.slug}`}>{item.text}</a>
{item.children.length > 0 &&
<ul>
{item.children.map((child,i) => (
<li key={i}>
<a href={`#${child.slug}`}>{child.text}</a>
</li>
))}
</ul>
}
{toc.length > 0 &&
(
<nav className="toc">
<h2>Table of Contents</h2>
<ol>
{toc.map((item, index) => (
<li key={index}>
<a href={`#${item.slug}`}>{item.text}</a>
{item.children.length > 0 &&
(
<ul>
{item.children.map((child, i) => (
<li key={i}>
<a href={`#${child.slug}`}>{child.text}</a>
</li>
))}
</ul>
)}
</li>
))}
</ol>
</nav>
)}
<article>
{children}
</article>
{footnotes.length > 0 &&
(
<ol className="footnotes">
{footnotes.map((note) => (
<li id={note.id}>
<span
dangerouslySetInnerHTML={{ __html: note.content }}
className="footnote-content"
/>
<a href={`#${note.refId}`} className="footnote-backref">
&#x21A9;
</a>
</li>
))}
</ol>
</nav>
}
<article>
{ children }
</article>
{ footnotes.length > 0 &&
<ol className="footnotes">
{footnotes.map(note => (
<li id={note.id}>
<span dangerouslySetInnerHTML={{ __html: note.content }} className="footnote-content"/>
<a href={`#${note.refId}`} className="footnote-backref">&#x21A9;</a>
</li>
))}
</ol>
}
)}
</>
);
}

View file

@ -3,13 +3,19 @@
description: About this website and credits where credit is due.
---
This is the personal website and blog of Foster Hangdaan and was created with [Lume](https://lume.land/) and [TypeScript](https://www.typescriptlang.org/).
This is the personal website and blog of Foster Hangdaan and was created with
[Lume](https://lume.land/) and [TypeScript](https://www.typescriptlang.org/).
The [source code](https://code.fosterhangdaan.com/foster/website) is under the GNU Affero General Public License (version 3 or later).
The [source code](https://code.fosterhangdaan.com/foster/website) is under the
GNU Affero General Public License (version 3 or later).
## Credits
- The icons are from [Tabler Icons](https://tabler-icons.io/) and [Simple Icons](https://simpleicons.org/).
- The colour palette of both the light theme and the dark theme is from [Tokyo Night](https://github.com/enkia/tokyo-night-vscode-theme).
- The sans-serif fonts are [Mona Sans and Hubot Sans](https://github.com/mona-sans).
- The monospace fonts are from the [Monaspace](https://monaspace.githubnext.com) font superfamily.
- The icons are from [Tabler Icons](https://tabler-icons.io/) and
[Simple Icons](https://simpleicons.org/).
- The colour palette of both the light theme and the dark theme is from
[Tokyo Night](https://github.com/enkia/tokyo-night-vscode-theme).
- The sans-serif fonts are
[Mona Sans and Hubot Sans](https://github.com/mona-sans).
- The monospace fonts are from the [Monaspace](https://monaspace.githubnext.com)
font superfamily.

View file

@ -1,10 +1,10 @@
export const title = "Blog";
export const description = "Hello, stranger. Stay a while and listen.";
export default function({ nav, comp }) {
export default function ({ nav, comp }) {
const { PostListItem } = comp;
const sortPosts = (a,b) => {
const sortPosts = (a, b) => {
if (a.data.date < b.data.date) {
return 1;
} else if (a.data.date > b.data.date) {
@ -17,16 +17,24 @@ export default function({ nav, comp }) {
if (!nav.menu("/blog/posts")) {
return (
<div className="no-posts">
<img src="https://static.fosterhangdaan.com/icons/tabler-icons/latest/svg/coffee.svg" className="icon" alt=""/>
<img
src="https://static.fosterhangdaan.com/icons/tabler-icons/latest/svg/coffee.svg"
className="icon"
alt=""
/>
<h2>No posts yet</h2>
<p>Foster is on a coffee break.<br/>Check back later.</p>
<p>
Foster is on a coffee break.<br />Check back later.
</p>
</div>
);
}
return (
<ul className="post-list">
{nav.menu("/blog/posts").children.sort(sortPosts).map((post,index) => <PostListItem key={index} post={post}/>)}
{nav.menu("/blog/posts").children.sort(sortPosts).map((post, index) => (
<PostListItem key={index} post={post} />
))}
</ul>
);
}

View file

@ -6,35 +6,61 @@
- design
---
Whether it's for UI design or theming, these colour palettes have played a great role in enhancing the eye-candy of my software projects. In fact, this very website uses one of the colour palettes on this list. I hope to share this list so that you may also find great use for these colour palettes in your own projects.
Whether it's for UI design or theming, these colour palettes have played a great
role in enhancing the eye-candy of my software projects. In fact, this very
website uses one of the colour palettes on this list. I hope to share this list
so that you may also find great use for these colour palettes in your own
projects.
## 4: Monokai
A warm, yet dark, colour palette created by [Wimer Hazenberg](https://monokai.nl/). Monokai is available in many text editors, terminal emulators, and (Linux) desktop environments.
A warm, yet dark, colour palette created by
[Wimer Hazenberg](https://monokai.nl/). Monokai is available in many text
editors, terminal emulators, and (Linux) desktop environments.
There is also a variant called [Monokai Pro](https://monokai.pro/) by the same author. This new palette is a modern interpretation of the classic palette and aims to improve functionality and legibility; perfect for coding.
There is also a variant called [Monokai Pro](https://monokai.pro/) by the same
author. This new palette is a modern interpretation of the classic palette and
aims to improve functionality and legibility; perfect for coding.
## 3: Dracula
[Dracula](https://draculatheme.com) is a dark colour palette created by [Zeno Rocha](https://zenorocha.com/) back in 2013. Since then, Dracula has grown to be one of the most popular colour palettes ever created and has been ported to many applications. There is no light variant available for this colour palette because Dracula is afraid of the light.
[Dracula](https://draculatheme.com) is a dark colour palette created by
[Zeno Rocha](https://zenorocha.com/) back in 2013. Since then, Dracula has grown
to be one of the most popular colour palettes ever created and has been ported
to many applications. There is no light variant available for this colour
palette because Dracula is afraid of the light.
Like Monokai, Dracula has a modern remake designed for terminal emulators and code editors called [Dracula Pro](https://draculatheme.com/pro).
Like Monokai, Dracula has a modern remake designed for terminal emulators and
code editors called [Dracula Pro](https://draculatheme.com/pro).
> Dracula Pro is a paid theme and its license must be purchased.
{.info}
> Dracula Pro is a paid theme and its license must be purchased. {.info}
## 2: Catppuccin
[Catppuccin](https://github.com/catppuccin/catppuccin.git) is a pastel colour palette which skyrocketed in popularity since its inception in 2021. It consists of 4 colour variants: Latte, Frappe, Macchiato and Mocha (with Macchiato being the main variant). Catppuccin is very popular in the Linux theming community due to its eye-pleasing pastel colours which pair nicely with pastel wallpapers.
[Catppuccin](https://github.com/catppuccin/catppuccin.git) is a pastel colour
palette which skyrocketed in popularity since its inception in 2021. It consists
of 4 colour variants: Latte, Frappe, Macchiato and Mocha (with Macchiato being
the main variant). Catppuccin is very popular in the Linux theming community due
to its eye-pleasing pastel colours which pair nicely with pastel wallpapers.
## 1: Tokyo Night
This is it... my favourite colour palette: [Tokyo Night](https://github.com/enkia/tokyo-night-vscode-theme.git). A colour palette made to celebrate the lights of downtown Tokyo at night. Unlike many of the other colour palettes on this list, Tokyo Night has a light theme among its three variants:
This is it... my favourite colour palette:
[Tokyo Night](https://github.com/enkia/tokyo-night-vscode-theme.git). A colour
palette made to celebrate the lights of downtown Tokyo at night. Unlike many of
the other colour palettes on this list, Tokyo Night has a light theme among its
three variants:
- **Night** - The main, dark variant.
- **Storm** - The same palette as Night except for a lighter background colour.
- **Light** - A light variant for those who prefer a light theme.
Remember I mentioned that this website uses one of the colour palettes on this list? Well, Tokyo Night is that colour palette. This website uses the Night variant for its dark theme and the Light variant for its light theme.
Remember I mentioned that this website uses one of the colour palettes on this
list? Well, Tokyo Night is that colour palette. This website uses the Night
variant for its dark theme and the Light variant for its light theme.
I also created an **unofficial** [Tokyo Night organization](https://code.fosterhangdaan.com/tokyo-night) to serve as a hub where one can find the ports made by others as well as ports made by myself. You can find a good amount of resources in that organization if you plan on using Tokyo Night in a project.
I also created an **unofficial**
[Tokyo Night organization](https://code.fosterhangdaan.com/tokyo-night) to serve
as a hub where one can find the ports made by others as well as ports made by
myself. You can find a good amount of resources in that organization if you plan
on using Tokyo Night in a project.

View file

@ -7,33 +7,53 @@
- guide
---
Throughout the years I have amassed a collection of laptops. Most of which are unused and collecting dust in storage. If you're reading this, chances are you're in the same situation. Like me, you're also trying to find some new purpose for that old, yet functional, piece of electronic heap. The answer: RetroPie. In this guide, I will show you how to turn an old laptop into a retro gaming station with RetroPie.
Throughout the years I have amassed a collection of laptops. Most of which are
unused and collecting dust in storage. If you're reading this, chances are
you're in the same situation. Like me, you're also trying to find some new
purpose for that old, yet functional, piece of electronic heap. The answer:
RetroPie. In this guide, I will show you how to turn an old laptop into a retro
gaming station with RetroPie.
The [RetroPie Setup Instructions for Debian](https://retropie.org.uk/docs/Debian) forms the foundation for this guide. You may refer to that documentation as a supplement.
The
[RetroPie Setup Instructions for Debian](https://retropie.org.uk/docs/Debian)
forms the foundation for this guide. You may refer to that documentation as a
supplement.
> If you get stuck, the best place to get help is by visiting the [RetroPie forum](https://retropie.org.uk/forum).
{.info}
> If you get stuck, the best place to get help is by visiting the
> [RetroPie forum](https://retropie.org.uk/forum). {.info}
## What is RetroPie?
[RetroPie](https://retropie.org.uk) allows you to turn a computer into a retro gaming station. It is mainly targeted for the [Raspberry Pi](https://www.raspberrypi.com) series of single-board computers; hence its name. Don't let that fool you, though. RetroPie can be used just as well to convert a PC or laptop into a retro gaming station.
[RetroPie](https://retropie.org.uk) allows you to turn a computer into a retro
gaming station. It is mainly targeted for the
[Raspberry Pi](https://www.raspberrypi.com) series of single-board computers;
hence its name. Don't let that fool you, though. RetroPie can be used just as
well to convert a PC or laptop into a retro gaming station.
## Prerequisites
- Laptop with [Debian 12.2](https://www.debian.org/News/2023/20231007) installed. Any [Debian](https://www.debian.org)-based distribution such as [Ubuntu](https://ubuntu.com) and [Linux Mint](https://www.linuxmint.com) should also work.
If you have multiple laptops to choose from, then I suggest selecting the fastest one available. The laptop's performance determines the types of emulators its able to run. On slow laptops, you will experience low framerates when running more demanding emulators such as [PCSX2](https://pcsx2.net) and [Dolphin](https://dolphin-emu.org).
- Laptop with [Debian 12.2](https://www.debian.org/News/2023/20231007)
installed. Any [Debian](https://www.debian.org)-based distribution such as
[Ubuntu](https://ubuntu.com) and [Linux Mint](https://www.linuxmint.com)
should also work.
If you have multiple laptops to choose from, then I suggest selecting the
fastest one available. The laptop's performance determines the types of
emulators its able to run. On slow laptops, you will experience low framerates
when running more demanding emulators such as [PCSX2](https://pcsx2.net) and
[Dolphin](https://dolphin-emu.org).
- Familiarity with Linux and its command line interface.
- Internet connection.
- A gaming controller. You can connect more after the installation.
I have tried using an [8BitDo Pro 2](https://www.8bitdo.com/pro2) and an Xbox 360 controller. RetroPie has great support for both of them.
I have tried using an [8BitDo Pro 2](https://www.8bitdo.com/pro2) and an Xbox
360 controller. RetroPie has great support for both of them.
- ROMs
I will not be going over how and where to obtain ROMs in this guide. For simplicity, I will be using a Gameboy Advance ROM in the examples.
I will not be going over how and where to obtain ROMs in this guide. For
simplicity, I will be using a Gameboy Advance ROM in the examples.
## RetroPie Setup
@ -41,13 +61,13 @@ ### Setup Debian
Update the system and packages:
``` sh
```sh
sudo apt update && sudo apt upgrade
```
Install RetroPie dependencies:
``` sh
```sh
sudo apt install git dialog unzip xmlstarlet
```
@ -55,19 +75,19 @@ ### Install RetroPie
Download the latest RetroPie script:
``` sh
```sh
git clone --depth=1 https://github.com/RetroPie/RetroPie-Setup.git
```
Enter the RetroPie directory:
``` sh
```sh
cd RetroPie-Setup
```
Execute the script:
``` sh
```sh
sudo ./retropie_setup.sh
```
@ -75,25 +95,33 @@ ### Install RetroPie
![Screenshot of installation main menu](021af5d6-cc50-4ac7-8734-750d2a2e3789.png)
Select **Basic Install** and press <kbd>Enter</kbd>. On the next screen, select **Yes** to start the installation process.
Select **Basic Install** and press <kbd>Enter</kbd>. On the next screen, select
**Yes** to start the installation process.
> The installation process compiles most of the emulators from source. This can take an hour or more on a slow laptop.
{.info}
> The installation process compiles most of the emulators from source. This can
> take an hour or more on a slow laptop. {.info}
You will be taken back to the main menu once the installation is complete. After the installation process has finished, exit the main menu and proceed to installing ROMs.
You will be taken back to the main menu once the installation is complete. After
the installation process has finished, exit the main menu and proceed to
installing ROMs.
### Install ROMs
Before running RetroPie, we should install some ROMs. ROMs are located in `~/RetroPie/roms`. It contains subdirectories for each supported system (GBA, Playstation, etc.).
Before running RetroPie, we should install some ROMs. ROMs are located in
`~/RetroPie/roms`. It contains subdirectories for each supported system (GBA,
Playstation, etc.).
For example, to install Gameboy Advance ROMs, place any ROM file in `~/RetroPie/roms/gba`:
For example, to install Gameboy Advance ROMs, place any ROM file in
`~/RetroPie/roms/gba`:
``` sh
```sh
cp pokemon-emerald.zip ~/RetroPie/roms/gba/
```
> The default Gameboy Advance emulator, **lr-mgba**, accepts ROM files in the following formats: `.7z`, `.gba` and `.zip`. Refer to [the documentation](https://retropie.org.uk/docs/Game-Boy-Advance) for a complete list.
{.info}
> The default Gameboy Advance emulator, **lr-mgba**, accepts ROM files in the
> following formats: `.7z`, `.gba` and `.zip`. Refer to
> [the documentation](https://retropie.org.uk/docs/Game-Boy-Advance) for a
> complete list. {.info}
## RetroPie First Startup
@ -103,30 +131,42 @@ ### Start RetroPie
![RetroPie on the GNOME app dashboard](be60a03a-aa41-48f9-b02f-5c8c728e66f6.png)
At first launch, Retropie will look for connected controllers and configure them. Connect your controller now and follow the on-screen instructions to configure it.
At first launch, Retropie will look for connected controllers and configure
them. Connect your controller now and follow the on-screen instructions to
configure it.
> Alternatively, if you don't have a controller, you can press and hold the <kbd>A</kbd> key on your keyboard to configure your keyboard as your controller.
{.info}
> Alternatively, if you don't have a controller, you can press and hold the
> <kbd>A</kbd> key on your keyboard to configure your keyboard as your
> controller. {.info}
![RetroPie controller setup](706ae312-eecc-470b-8b3a-429c220962af.png)
### Launch a Game
The main menu will display only the systems for which there are available ROMs. Since we installed a Gameboy Advance ROM in a previous step, the Gameboy Advance system should be displayed on the menu.
The main menu will display only the systems for which there are available ROMs.
Since we installed a Gameboy Advance ROM in a previous step, the Gameboy Advance
system should be displayed on the menu.
![RetroPie main menu](14701639-0179-4d2c-934f-ad3a63e6f85e.png)
Our game should appear within the Gameboy Advance game selection screen. Launch the game by selecting it then pressing the <kbd>A</kbd> button (or the equivalent) on your controller.
Our game should appear within the Gameboy Advance game selection screen. Launch
the game by selecting it then pressing the <kbd>A</kbd> button (or the
equivalent) on your controller.
![Gameboy Advance game selection screen](6b9c2f7a-773d-4bac-a6f8-69668fba80dc.png)
## Conclusion
Hopefully, the installation went well and your old laptop is now a bonafide retro gaming station capable of playing a variety of classic games.
Hopefully, the installation went well and your old laptop is now a bonafide
retro gaming station capable of playing a variety of classic games.
If you are having issues, refer to the [Troubleshooting section](#troubleshooting), the [RetroPie forums](https://retropie.org.uk/forum) or the [RetroPie documentation](https://retropie.org.uk/docs).
If you are having issues, refer to the
[Troubleshooting section](#troubleshooting), the
[RetroPie forums](https://retropie.org.uk/forum) or the
[RetroPie documentation](https://retropie.org.uk/docs).
For some ideas on what you can do after installing RetroPie, checkout the [Post Installation section](#post-installation).
For some ideas on what you can do after installing RetroPie, checkout the
[Post Installation section](#post-installation).
[Contact me](/#contact-me) for any comments or suggestions regarding this guide.
@ -134,37 +174,57 @@ ## Post Installation
A few things you can do after installing RetroPie:
- If your laptop has an HDMI or DisplayPort port, connect it to a TV or an external monitor for a larger screen.
- Add your installed PC games from [Steam](https://store.steampowered.com) or [GoG](https://www.gog.com).
- If your laptop has an HDMI or DisplayPort port, connect it to a TV or an
external monitor for a larger screen.
- Add your installed PC games from [Steam](https://store.steampowered.com) or
[GoG](https://www.gog.com).
RetroPie can launch installed PC games directly from the RetroPie menu without leaving or closing its window. This involves adding a custom system which launches the games via a shell script. I'll be creating separate guide on how to do this. So stay tuned!
RetroPie can launch installed PC games directly from the RetroPie menu without
leaving or closing its window. This involves adding a custom system which
launches the games via a shell script. I'll be creating separate guide on how
to do this. So stay tuned!
- Setup Debian to [auto login at boot](https://retropie.org.uk/docs/Debian/?h=autologin#ubuntu-does-not-autologin) and RetroPie to [auto launch at login](https://retropie.org.uk/docs/Debian/#configure-retropie). This removes the manual intervention needed to log in and launch RetroPie after booting up.
- Use the [scraper](https://retropie.org.uk/docs/Scraper/?h=scraper) to populate the meta information and artwork for your games.
- Adjust the power settings for a better gaming experience. For example, you can prevent the screen from turning off or prevent the laptop from going to sleep when it's idle.
- Setup Debian to
[auto login at boot](https://retropie.org.uk/docs/Debian/?h=autologin#ubuntu-does-not-autologin)
and RetroPie to
[auto launch at login](https://retropie.org.uk/docs/Debian/#configure-retropie).
This removes the manual intervention needed to log in and launch RetroPie
after booting up.
- Use the [scraper](https://retropie.org.uk/docs/Scraper/?h=scraper) to populate
the meta information and artwork for your games.
- Adjust the power settings for a better gaming experience. For example, you can
prevent the screen from turning off or prevent the laptop from going to sleep
when it's idle.
- Install more ROMs and build your retro game collection.
- Connect more controllers for multiplayer.
## Troubleshooting
This section goes over how to resolve issues not available in the [FAQ section of the Debian installation instructions](https://retropie.org.uk/docs/Debian/?h=debian#faq).
This section goes over how to resolve issues not available in the
[FAQ section of the Debian installation instructions](https://retropie.org.uk/docs/Debian/?h=debian#faq).
### Top Bar Cuts Off Window When Fullscreen
On Debian 12.2, the top bar still occupies space even when the RetroPie window is fullscreen. This pushes the RetroPie window downwards cutting off its bottom portion.
On Debian 12.2, the top bar still occupies space even when the RetroPie window
is fullscreen. This pushes the RetroPie window downwards cutting off its bottom
portion.
![RetroPie fullscreen cut-off](300394f7-bd14-4ffa-8cc6-2eaf6966d728.png)
This seems to be caused by the default display protocol, Wayland. So a simple solution is to switch the display protocol from Wayland to Xorg.
This seems to be caused by the default display protocol, Wayland. So a simple
solution is to switch the display protocol from Wayland to Xorg.
Begin by logging out to get to the login screen. On the login screen, select your user.
Begin by logging out to get to the login screen. On the login screen, select
your user.
![Debian login screen](ced2816f-7ffd-478f-b86d-e5062caa970b.png)
Click the gear icon at the bottom-right of the screen to open a menu. Select **GNOME on Xorg**.
Click the gear icon at the bottom-right of the screen to open a menu. Select
**GNOME on Xorg**.
![Display server settings](1450cb87-24e7-468f-b136-42bb6b989615.png)
Log back in then launch RetroPie. Its window should now be displayed in its entirety.
Log back in then launch RetroPie. Its window should now be displayed in its
entirety.
![RetroPie fullscreen no cut-off](14701639-0179-4d2c-934f-ad3a63e6f85e.png)

View file

@ -7,46 +7,79 @@
- automation
---
Ever tried hosting a server in your home network but annoyed at the fact that your public IP address keeps changing? Imagine for a moment that you're running a [Nextcloud](https://nextcloud.com) server located at `nextcloud.example.com`. All of a sudden, the public IP changes and your domain is still pointing to the old IP address. As a result, connectivity to your server gets disrupted. The change can happen at any time and without warning. It can take some time for you to find out. Once you do, you'll have to manually update the [A-record](https://en.wikipedia.org/wiki/List_of_DNS_record_types#A) of `nextcloud.example.com` to restore connectivity.
Ever tried hosting a server in your home network but annoyed at the fact that
your public IP address keeps changing? Imagine for a moment that you're running
a [Nextcloud](https://nextcloud.com) server located at `nextcloud.example.com`.
All of a sudden, the public IP changes and your domain is still pointing to the
old IP address. As a result, connectivity to your server gets disrupted. The
change can happen at any time and without warning. It can take some time for you
to find out. Once you do, you'll have to manually update the
[A-record](https://en.wikipedia.org/wiki/List_of_DNS_record_types#A) of
`nextcloud.example.com` to restore connectivity.
As you can see, spontaneous IP address changes are disruptive events; a [static IP address](https://en.wikipedia.org/wiki/IP_address#IP_address_assignment) or a [DDNS](https://en.wikipedia.org/wiki/Dynamic_DNS) service is essential for running public servers. Internet service providers usually offer static IP addresses only in their business plans. So this leaves out the vast majority of internet users. The other option, DDNS, is a feature supported by some routers. However, utilizing this feature typically involves having to sign-up for a hostname on a DDNS provider like [No-IP](https://www.noip.com). If you get one of No-IP's free accounts, then you must [manually confirm hostnames](https://www.noip.com/support/knowledgebase/confirm-my-hostname-free-account-support-question-day) every 30 days.
As you can see, spontaneous IP address changes are disruptive events; a
[static IP address](https://en.wikipedia.org/wiki/IP_address#IP_address_assignment)
or a [DDNS](https://en.wikipedia.org/wiki/Dynamic_DNS) service is essential for
running public servers. Internet service providers usually offer static IP
addresses only in their business plans. So this leaves out the vast majority of
internet users. The other option, DDNS, is a feature supported by some routers.
However, utilizing this feature typically involves having to sign-up for a
hostname on a DDNS provider like [No-IP](https://www.noip.com). If you get one
of No-IP's free accounts, then you must
[manually confirm hostnames](https://www.noip.com/support/knowledgebase/confirm-my-hostname-free-account-support-question-day)
every 30 days.
So, the options we have thus far require us to either:
- Pay for a more expensive internet plan.
- Utilize a router feature that requires creating a hostname on a third-party DDNS provider.
- Utilize a router feature that requires creating a hostname on a third-party
DDNS provider.
If only there was another way...
## The Solution
The answer is to create our own DDNS. We can achieve a DDNS-like system with a [shell script](https://en.wikipedia.org/wiki/Shell_script) that runs every hour; scheduled via [Cron](https://en.wikipedia.org/wiki/Cron). The script performs the following tasks everytime it runs:
The answer is to create our own DDNS. We can achieve a DDNS-like system with a
[shell script](https://en.wikipedia.org/wiki/Shell_script) that runs every hour;
scheduled via [Cron](https://en.wikipedia.org/wiki/Cron). The script performs
the following tasks everytime it runs:
1. Obtain the current public IP address and the domain's A-record.
1. Compare the public IP and the A-record's value.
1. If they are not the same, it updates the A-record to the value of the public IP address.
1. If they are not the same, it updates the A-record to the value of the public
IP address.
This system provides some major advantages:
- Can be deployed on any computer with a modern [Linux](https://en.wikipedia.org/wiki/Linux) installation.
- Can be deployed on any computer with a modern
[Linux](https://en.wikipedia.org/wiki/Linux) installation.
- Does not depend on a DDNS provider.
- No need for a router with DDNS support.
- The script is flexible and can be changed to use another [domain registrar](https://en.wikipedia.org/wiki/Domain_name_registrar).
- The script is flexible and can be changed to use another
[domain registrar](https://en.wikipedia.org/wiki/Domain_name_registrar).
## Requirements
- Basic proficiency with Linux and its command line.
- A computer with a [Debian](https://www.debian.org)-based distribution installed.
- A computer with a [Debian](https://www.debian.org)-based distribution
installed.
This computer will be referred to as the *Host Computer*. It will be online 24/7 to keep the scheduled script running.
This computer will be referred to as the _Host Computer_. It will be online
24/7 to keep the scheduled script running.
- Domain (or subdomain) with its DNS A-record set.
Your domain registrar must have an [API](https://en.wikipedia.org/wiki/API) which provides the ability to read and update records. This guide will be using the [GoDaddy API](https://developer.godaddy.com/doc) to update an A-record on [GoDaddy](https://www.godaddy.com).
Your domain registrar must have an [API](https://en.wikipedia.org/wiki/API)
which provides the ability to read and update records. This guide will be
using the [GoDaddy API](https://developer.godaddy.com/doc) to update an
A-record on [GoDaddy](https://www.godaddy.com).
- **Optional**: Familiarity with [JavaScript](https://en.wikipedia.org/wiki/JavaScript) or [TypeScript](https://en.wikipedia.org/wiki/TypeScript).
- **Optional**: Familiarity with
[JavaScript](https://en.wikipedia.org/wiki/JavaScript) or
[TypeScript](https://en.wikipedia.org/wiki/TypeScript).
Having some basic TypeScript knowledge allows you to modify or update the shell script.
Having some basic TypeScript knowledge allows you to modify or update the
shell script.
## Preparation
@ -58,42 +91,48 @@ #### Update the System
Open a terminal then enter the command below to update the system.
``` sh
```sh
sudo apt -y update && sudo apt -y upgrade
```
#### Install Cron
Cron is included in most Linux distributions but we will install it just to be sure:
Cron is included in most Linux distributions but we will install it just to be
sure:
``` sh
```sh
sudo apt install cron
```
#### Install Deno
[Deno](https://deno.com) is a secure runtime environment for JavaScript and TypeScript. It is the environment for which the TypeScript-programmed shell script will be running under.
[Deno](https://deno.com) is a secure runtime environment for JavaScript and
TypeScript. It is the environment for which the TypeScript-programmed shell
script will be running under.
At the time of writing, Deno is not available in the [APT](https://en.wikipedia.org/wiki/APT_(software)) repositories; we will be using the installation script instead.
At the time of writing, Deno is not available in the
[APT](https://en.wikipedia.org/wiki/APT_(software)) repositories; we will be
using the installation script instead.
> Refer to the Deno documentation for [additional installation options](https://docs.deno.com/runtime/manual/getting_started/installation).
{.info}
> Refer to the Deno documentation for
> [additional installation options](https://docs.deno.com/runtime/manual/getting_started/installation).
> {.info}
Run the following command to install Deno:
``` sh
```sh
curl -fsSL https://deno.land/x/install/install.sh | sh
```
Confirm that Deno is properly installed by checking its version:
``` sh
```sh
deno --version
```
An output like the one below indicates that Deno was successfully installed:
``` txt
```txt
deno 1.38.3 (release, x86_64-unknown-linux-gnu)
v8 12.0.267.1
typescript 5.2.2
@ -101,10 +140,12 @@ #### Install Deno
### GoDaddy API Keys
Follow the [GoDaddy API setup](https://developer.godaddy.com/getstarted#setup) to create the API secret and key. Take note of the key and secret as they will be needed shortly.
Follow the [GoDaddy API setup](https://developer.godaddy.com/getstarted#setup)
to create the API secret and key. Take note of the key and secret as they will
be needed shortly.
> Ensure that you get the API secret and key for the **production environment**; not the ones for the test environment.
{.warning}
> Ensure that you get the API secret and key for the **production environment**;
> not the ones for the test environment. {.warning}
## The Script
@ -113,16 +154,18 @@ ## The Script
> - [GET /v1/domains/{domain}/records/{type}/{name}](https://developer.godaddy.com/doc/endpoint/domains#/v1/recordGet)
> - [PUT /v1/domains/{domain}/records/{type}/{name}](https://developer.godaddy.com/doc/endpoint/domains#/v1/recordReplaceTypeName)
>
> Refer to the [GoDaddy Domains API documentation](https://developer.godaddy.com/doc/endpoint/domains) for the complete list of endpoints.
{.info}
> Refer to the
> [GoDaddy Domains API documentation](https://developer.godaddy.com/doc/endpoint/domains)
> for the complete list of endpoints. {.info}
> The public IP address is obtained from an instance of [IpMe](https://code.fosterhangdaan.com/foster/ipme) at [https://ipme.fosterhangdaan.com](https://ipme.fosterhangdaan.com). [Ipify](https://www.ipify.org) can be used as an alternative.
{.info}
> The public IP address is obtained from an instance of
> [IpMe](https://code.fosterhangdaan.com/foster/ipme) at
> [https://ipme.fosterhangdaan.com](https://ipme.fosterhangdaan.com).
> [Ipify](https://www.ipify.org) can be used as an alternative. {.info}
Here is the content of the script:
``` ts
```ts
#!/usr/bin/env -S deno run --allow-net
/**
@ -148,15 +191,16 @@ ## The Script
const domainParts = domain.split(".");
const domainRoot = domainParts.slice(-2).join(".");
const domainName = domainParts.slice(0,-2).join(".") || "@";
const domainName = domainParts.slice(0, -2).join(".") || "@";
const goDaddyUrl = `https://api.godaddy.com/v1/domains/${domainRoot}/records/A/${domainName}`;
const goDaddyUrl =
`https://api.godaddy.com/v1/domains/${domainRoot}/records/A/${domainName}`;
const goDaddyRequestHeaders = new Headers({
Authorization: `sso-key ${GODADDY_API_KEY}:${GODADDY_API_SECRET}`,
});
const [ publicIpResponse, goDaddyResponse ] = await Promise.all([
const [publicIpResponse, goDaddyResponse] = await Promise.all([
fetch(publicIpUrl),
fetch(goDaddyUrl, { headers: goDaddyRequestHeaders }),
]);
@ -177,7 +221,9 @@ ## The Script
const goDaddyIP = goDaddyJsonResponse[0]["data"];
if (publicIP !== goDaddyIP) {
console.log(`The public IP address has changed. Public IP: ${publicIP}; Old IP: ${goDaddyIP}`);
console.log(
`The public IP address has changed. Public IP: ${publicIP}; Old IP: ${goDaddyIP}`,
);
goDaddyRequestHeaders.append("Content-Type", "application/json");
const response = await fetch(goDaddyUrl, {
method: "PUT",
@ -185,7 +231,7 @@ ## The Script
body: JSON.stringify([
{
data: publicIP,
}
},
]),
});
if (!response.ok) {
@ -198,28 +244,37 @@ ## The Script
## Schedule the Script to Run Every Hour with Cron
On the Host Computer, save [the script](#the-script) as a file at `/etc/cron.hourly/ddns.ts`. Make sure to replace some necessary values:
On the Host Computer, save [the script](#the-script) as a file at
`/etc/cron.hourly/ddns.ts`. Make sure to replace some necessary values:
- Replace `example.com` in `const domain = "example.com";` with your GoDaddy domain.
- Replace `example.com` in `const domain = "example.com";` with your GoDaddy
domain.
- Replace `key` in `const GODADDY_API_KEY = "key";` with your GoDaddy API key.
- Replace `secret` in `const GODADDY_API_SECRET = "secret";` with your GoDaddy API secret.
- Replace `secret` in `const GODADDY_API_SECRET = "secret";` with your GoDaddy
API secret.
Grant read, write, and execute permissions only to the `root` user:
``` sh
```sh
sudo chown root:root /etc/cron.hourly/ddns.ts
sudo chmod 700 /etc/cron.hourly/ddns.ts
```
> Access to the script should be granted only to authorized users (such as `root`) since the script contains sensitive information: your GoDaddy API secret and key.
{.warning}
> Access to the script should be granted only to authorized users (such as
> `root`) since the script contains sensitive information: your GoDaddy API
> secret and key. {.warning}
The script should now be scheduled to run every hour by Cron.
## Conclusion
In this guide, we have setup an hourly Cron script that updates a GoDaddy domain's A-record if the public IP address changed. You can now rest assured that your hosted services will be protected from disruptions caused by public IP address changes.
In this guide, we have setup an hourly Cron script that updates a GoDaddy
domain's A-record if the public IP address changed. You can now rest assured
that your hosted services will be protected from disruptions caused by public IP
address changes.
Feel free to modify the script to your needs; especially if you are using a domain registrar other than GoDaddy.
Feel free to modify the script to your needs; especially if you are using a
domain registrar other than GoDaddy.
As always, [contact me](/#contact-me) for any comments or suggestions regarding this guide.
As always, [contact me](/#contact-me) for any comments or suggestions regarding
this guide.

View file

@ -3,20 +3,21 @@
description: Information about my GPG public key and how to obtain it.
---
> I do not use keyservers. A key claiming to be mine from a keyserver is definitely a fake.
{.warning}
> I do not use keyservers. A key claiming to be mine from a keyserver is
> definitely a fake. {.warning}
> I suggest reading up on GPG if you are unfamiliar with it.
> You can find information about GPG on the [GnuPG official website](https://gnupg.org/).
{.info}
> I suggest reading up on GPG if you are unfamiliar with it. You can find
> information about GPG on the [GnuPG official website](https://gnupg.org/).
> {.info}
## Obtaining My Key
You can download my public key here: [Foster Hangdaan's Public Key](https://static.fosterhangdaan.com/foster-pubkey.asc){download}.
You can download my public key here:
[Foster Hangdaan's Public Key](https://static.fosterhangdaan.com/foster-pubkey.asc){download}.
The key's fingerprint should match the one below:
``` txt
```txt
pub ed25519/E48D7F49A852F112 2023-07-14 [SC]
Key fingerprint = DBD3 8E38 4B9E 1F4F 19F9 5BAE E48D 7F49 A852 F112
uid Foster Hangdaan <foster@hangdaan.email>
@ -24,6 +25,11 @@ ## Obtaining My Key
## Utilizing My Key
Once you have imported my public key, you can use it to verify software and binaries released by me.
Once you have imported my public key, you can use it to verify software and
binaries released by me.
You can also use my public key to encrypt emails you send to me. In that case, I would also need your public key so I can encrypt the emails I send back to you. The excellent [Email Self-Defense Guide by the Free Software Foundation](https://emailselfdefense.fsf.org/) describes how this process works.
You can also use my public key to encrypt emails you send to me. In that case, I
would also need your public key so I can encrypt the emails I send back to you.
The excellent
[Email Self-Defense Guide by the Free Software Foundation](https://emailselfdefense.fsf.org/)
describes how this process works.

View file

@ -1,7 +1,7 @@
export const description = "Software developer and open-source enthusiast.";
export default function({ nav }) {
const sortPosts = (a,b) => {
export default function ({ nav }) {
const sortPosts = (a, b) => {
if (a.data.date < b.data.date) {
return 1;
} else if (a.data.date > b.data.date) {
@ -14,51 +14,85 @@ export default function({ nav }) {
return (
<>
<p>
I am a software developer, open-source enthusiast, lover of pizza, and renegade of funk. I speak fluent English and Filipino.
I am a software developer, open-source enthusiast, lover of pizza, and
renegade of funk. I speak fluent English and Filipino.
</p>
<p>
You'll most likely find me within <a href="https://code.fosterhangdaan.com">my lab</a> tinkering with my inventions and the latest JavaScript frameworks.
Other times, I help in the battle for an open web and for user privacy by contributing in the development of free and open-source software.
You'll most likely find me within{" "}
<a href="https://code.fosterhangdaan.com">my lab</a>{" "}
tinkering with my inventions and the latest JavaScript frameworks. Other
times, I help in the battle for an open web and for user privacy by
contributing in the development of free and open-source software.
</p>
<h2 id="contact-me" tabIndex="-1">
<a className="header-anchor" href="#contact-me">Contact Me</a>
</h2>
<p>
The best method of reaching me is through my email: <a href="mailto:foster@hangdaan.email">foster@hangdaan.email</a>.
The best method of reaching me is through my email:{" "}
<a href="mailto:foster@hangdaan.email">foster@hangdaan.email</a>.
</p>
<p>
If you'd like an encrypted response, you can send me your GPG public key. You can find mine in the <a href="/gpg-key/">GPG Key</a> page.
If you'd like an encrypted response, you can send me your GPG public
key. You can find mine in the <a href="/gpg-key/">GPG Key</a> page.
</p>
<h2 id="highlighted-projects" tabIndex="-1">
<a className="header-anchor" href="#highlighted-projects">Highlighted Projects</a>
<a className="header-anchor" href="#highlighted-projects">
Highlighted Projects
</a>
</h2>
<ul>
<li>
<a href="https://code.fosterhangdaan.com/foster/website">This website</a> &mdash; My personal website &amp; blog.
<a href="https://code.fosterhangdaan.com/foster/website">
This website
</a>{" "}
&mdash; My personal website &amp; blog.
</li>
<li>
<a href="https://code.fosterhangdaan.com/foster/grub-themes">GRUB Themes</a> &mdash; Collection of themes for the <strong>GR</strong>and <strong>U</strong>nified <strong>B</strong>ootloader.
<a href="https://code.fosterhangdaan.com/foster/grub-themes">
GRUB Themes
</a>{" "}
&mdash; Collection of themes for the <strong>GR</strong>and{" "}
<strong>U</strong>nified <strong>B</strong>ootloader.
</li>
<li>
<a href="https://code.fosterhangdaan.com/foster/ipme">IpMe</a> &mdash; A self-hostable API for obtaining your public IP address.
<a href="https://code.fosterhangdaan.com/foster/ipme">IpMe</a>{" "}
&mdash; A self-hostable API for obtaining your public IP address.
</li>
<li>
<a href="https://code.fosterhangdaan.com/foster/bitcoin-core-container">Bitcoin Core Container</a> &mdash; A containerized Bitcoin node.
<a href="https://code.fosterhangdaan.com/foster/bitcoin-core-container">
Bitcoin Core Container
</a>{" "}
&mdash; A containerized Bitcoin node.
</li>
<li>
<a href="https://code.fosterhangdaan.com/foster/monero-node-container">Monero Node Container</a> &mdash; A containerized Monero node.
<a href="https://code.fosterhangdaan.com/foster/monero-node-container">
Monero Node Container
</a>{" "}
&mdash; A containerized Monero node.
</li>
</ul>
<p>
<a href="https://code.fosterhangdaan.com/foster?tab=repositories">View all projects</a>
<a href="https://code.fosterhangdaan.com/foster?tab=repositories">
View all projects
</a>
</p>
<h2 id="latest-blog-posts" tabIndex="-1">
<a className="header-anchor" href="#latest-blog-posts">Latest Blog Posts</a>
<a className="header-anchor" href="#latest-blog-posts">
Latest Blog Posts
</a>
</h2>
<ul>
{nav.menu("/blog/posts").children.sort(sortPosts).slice(0,5).map((post,index) => (
{nav.menu("/blog/posts").children.sort(sortPosts).slice(0, 5).map((
post,
index,
) => (
<li key={index}>
<a href={post.data.url} >{post.data.title}</a> &mdash; <time className="post-list-date">{Intl.DateTimeFormat("en-CA", { dateStyle: "long" }).format(post.data.date)}</time>
<a href={post.data.url}>{post.data.title}</a> &mdash;{" "}
<time className="post-list-date">
{Intl.DateTimeFormat("en-CA", { dateStyle: "long" }).format(
post.data.date,
)}
</time>
</li>
))}
</ul>

View file

@ -7,33 +7,32 @@ ## Experience
### Startup Tech Pros
Tech Lead<br/>
June 2022 - present
Tech Lead<br/> June 2022 - present
- Set standards and procedures for Engineering department.
- Design, maintain and deploy cloud infrastructure.
- Manage software development projects.
- Determine software tools and services based on company budget and requirements.
- Collaborate with clients to create websites and infrastructure based on business needs.
- Determine software tools and services based on company budget and
requirements.
- Collaborate with clients to create websites and infrastructure based on
business needs.
### Zetane Systems
Full-Stack Web Developer<br/>
October 2021 - December 2023
Full-Stack Web Developer<br/> October 2021 - December 2023
- Develop pages and components in Next.js according to Figma designs.
- Create CI/CD pipeline to package application and deploy to AWS.
- Ensure application quality and functionality by writing end-to-end tests using Cypress.
- Ensure application quality and functionality by writing end-to-end tests using
Cypress.
- Refactored code and design of company website.
## Education
### Computer Engineering Technology
Emphasis in Computer Programming<br/>
Seneca College of Applied Arts and Technology<br/>
Graduated August 2020<br/>
Cumulative GPA 4.0/4.0<br/>
Emphasis in Computer Programming<br/> Seneca College of Applied Arts and
Technology<br/> Graduated August 2020<br/> Cumulative GPA 4.0/4.0<br/>
## Skills