Format code
Used `deno fmt`.
This commit is contained in:
parent
dd69d2a761
commit
5b2d1d93e4
16 changed files with 501 additions and 229 deletions
|
@ -2,13 +2,21 @@ interface Props {
|
||||||
post: any;
|
post: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function(props: Props) {
|
export default function (props: Props) {
|
||||||
return (
|
return (
|
||||||
<li className="post-list-item">
|
<li className="post-list-item">
|
||||||
<a href={props.post.data.url} className="post-list-title">{props.post.data.title}</a>
|
<a href={props.post.data.url} className="post-list-title">
|
||||||
<time className="post-list-date">{Intl.DateTimeFormat("en-CA", { dateStyle: "long" }).format(props.post.data.date)}</time>
|
{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">
|
<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>
|
</ul>
|
||||||
<p className="post-list-description">{props.post.data.description}</p>
|
<p className="post-list-description">{props.post.data.description}</p>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export default function({ comp }) {
|
export default function ({ comp }) {
|
||||||
const iconStyle = {
|
const iconStyle = {
|
||||||
filter: "var(--filter-fg)",
|
filter: "var(--filter-fg)",
|
||||||
};
|
};
|
||||||
|
@ -31,7 +31,10 @@ export default function({ comp }) {
|
||||||
alt="rss"
|
alt="rss"
|
||||||
/>
|
/>
|
||||||
</a>
|
</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
|
<img
|
||||||
src="https://static.fosterhangdaan.com/icons/tabler-icons/latest/svg/braces.svg"
|
src="https://static.fosterhangdaan.com/icons/tabler-icons/latest/svg/braces.svg"
|
||||||
className="icon"
|
className="icon"
|
||||||
|
@ -41,7 +44,18 @@ export default function({ comp }) {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<p>Copyright © 2023 Foster Hangdaan</p>
|
<p>Copyright © 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>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,18 +8,21 @@ export interface Props {
|
||||||
comp: any;
|
comp: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function(props: Props) {
|
export default function (props: Props) {
|
||||||
const dateFormatted = Intl.DateTimeFormat("en-CA", { dateStyle: "long" }).format(props.date);
|
const dateFormatted = Intl.DateTimeFormat("en-CA", { dateStyle: "long" })
|
||||||
|
.format(props.date);
|
||||||
return (
|
return (
|
||||||
<header className="page-header">
|
<header className="page-header">
|
||||||
<h1>{ props.title }</h1>
|
<h1>{props.title}</h1>
|
||||||
{props.author &&
|
{props.author &&
|
||||||
<p className="author" style={{ color: "var(--color-brown)" }}>
|
(
|
||||||
By {props.author.name} on <time dateTime={props.date.toISOString()}>{dateFormatted}</time>
|
<p className="author" style={{ color: "var(--color-brown)" }}>
|
||||||
</p>
|
By {props.author.name} on{" "}
|
||||||
}
|
<time dateTime={props.date.toISOString()}>{dateFormatted}</time>
|
||||||
<p className="subheading">{ props.description }</p>
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="subheading">{props.description}</p>
|
||||||
{props.comp.separator()}
|
{props.comp.separator()}
|
||||||
</header>
|
</header>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export default function() {
|
export default function () {
|
||||||
const iconStyle = {
|
const iconStyle = {
|
||||||
filter: "var(--filter-green)",
|
filter: "var(--filter-green)",
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export default function () {
|
export default function () {
|
||||||
return (
|
return (
|
||||||
<div className="separator">
|
<div className="separator">
|
||||||
<span className="outer-outline"/>
|
<span className="outer-outline" />
|
||||||
<span>
|
<span>
|
||||||
<img
|
<img
|
||||||
src="https://static.fosterhangdaan.com/icons/tabler-icons/latest/svg/diamonds.svg"
|
src="https://static.fosterhangdaan.com/icons/tabler-icons/latest/svg/diamonds.svg"
|
||||||
|
@ -10,7 +10,7 @@ export default function () {
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span className="outer-outline"/>
|
<span className="outer-outline" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
20
_config.ts
20
_config.ts
|
@ -44,14 +44,14 @@ site.use(feed({
|
||||||
description: "=description",
|
description: "=description",
|
||||||
published: "=date",
|
published: "=date",
|
||||||
updated: "=updated",
|
updated: "=updated",
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
site.use(code_highlight({
|
site.use(code_highlight({
|
||||||
languages: {
|
languages: {
|
||||||
typescript: lang_typescript,
|
typescript: lang_typescript,
|
||||||
javascript: lang_javascript,
|
javascript: lang_javascript,
|
||||||
bash: lang_bash,
|
bash: lang_bash,
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
site.use(toc({
|
site.use(toc({
|
||||||
slugify: {
|
slugify: {
|
||||||
|
@ -66,13 +66,15 @@ site.process([".html"], (pages) => {
|
||||||
// NOTE: This is a hack to append a class to JS doctrings so that we
|
// 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
|
// can style them. If only the Hightlight.js plugin could be configured
|
||||||
// to do this instead.
|
// to do this instead.
|
||||||
page.document?.getElementsByClassName("hljs-comment").forEach((codeCommentElement) => {
|
page.document?.getElementsByClassName("hljs-comment").forEach(
|
||||||
const docStringRegex = /^\/\*\*.*\*\/$/gsm;
|
(codeCommentElement) => {
|
||||||
const matchResult = codeCommentElement.innerText.match(docStringRegex);
|
const docStringRegex = /^\/\*\*.*\*\/$/gsm;
|
||||||
if (matchResult) {
|
const matchResult = codeCommentElement.innerText.match(docStringRegex);
|
||||||
codeCommentElement.classList.add("docstring");
|
if (matchResult) {
|
||||||
}
|
codeCommentElement.classList.add("docstring");
|
||||||
});
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
return (
|
||||||
<html lang="en-CA">
|
<html lang="en-CA">
|
||||||
<head>
|
<head>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta charSet="utf-8"/>
|
<meta charSet="utf-8" />
|
||||||
<meta name="description" content={description}/>
|
<meta name="description" content={description} />
|
||||||
<meta name="author" content="Foster Hangdaan"/>
|
<meta name="author" content="Foster Hangdaan" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="color-scheme" content="dark light"/>
|
<meta name="color-scheme" content="dark light" />
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1a1b26"/>
|
<meta
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#d5d6db"/>
|
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 &&
|
{Array.isArray(metas) && metas.length > 0 &&
|
||||||
metas.map((m,index) => <meta key={index} name={m.name} content={m.content}/>)
|
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="stylesheet" href="/styles/main.css" />
|
||||||
<link rel="icon" type="image/png" href="/icons/tabicon-32.png" sizes="32x32" />
|
<link
|
||||||
<link rel="icon" type="image/png" href="/icons/tabicon-96.png" sizes="96x96" />
|
rel="icon"
|
||||||
<link rel="icon" type="image/png" href="/icons/tabicon-128.png" sizes="128x128" />
|
type="image/png"
|
||||||
<link rel="icon" type="image/png" href="/icons/tabicon-196.png" sizes="196x196" />
|
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 &&
|
{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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{ comp.navbar() }
|
{comp.navbar()}
|
||||||
{ comp.header({title, description, author, date}) }
|
{comp.header({ title, description, author, date })}
|
||||||
<main className="main-content">{children}</main>
|
<main className="main-content">{children}</main>
|
||||||
{ comp.footer() }
|
{comp.footer()}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,42 +1,50 @@
|
||||||
export const layout = "./base.tsx";
|
export const layout = "./base.tsx";
|
||||||
|
|
||||||
export default function({ children, toc, footnotes }) {
|
export default function ({ children, toc, footnotes }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ toc.length > 0 &&
|
{toc.length > 0 &&
|
||||||
<nav className="toc">
|
(
|
||||||
<h2>Table of Contents</h2>
|
<nav className="toc">
|
||||||
<ol>
|
<h2>Table of Contents</h2>
|
||||||
{toc.map((item,index) => (
|
<ol>
|
||||||
<li key={index}>
|
{toc.map((item, index) => (
|
||||||
<a href={`#${item.slug}`}>{item.text}</a>
|
<li key={index}>
|
||||||
{item.children.length > 0 &&
|
<a href={`#${item.slug}`}>{item.text}</a>
|
||||||
<ul>
|
{item.children.length > 0 &&
|
||||||
{item.children.map((child,i) => (
|
(
|
||||||
<li key={i}>
|
<ul>
|
||||||
<a href={`#${child.slug}`}>{child.text}</a>
|
{item.children.map((child, i) => (
|
||||||
</li>
|
<li key={i}>
|
||||||
))}
|
<a href={`#${child.slug}`}>{child.text}</a>
|
||||||
</ul>
|
</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">
|
||||||
|
↩
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</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">↩</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
18
about.md
18
about.md
|
@ -3,13 +3,19 @@
|
||||||
description: About this website and credits where credit is due.
|
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
|
## Credits
|
||||||
|
|
||||||
- The icons are from [Tabler Icons](https://tabler-icons.io/) and [Simple Icons](https://simpleicons.org/).
|
- The icons are from [Tabler Icons](https://tabler-icons.io/) and
|
||||||
- The colour palette of both the light theme and the dark theme is from [Tokyo Night](https://github.com/enkia/tokyo-night-vscode-theme).
|
[Simple Icons](https://simpleicons.org/).
|
||||||
- The sans-serif fonts are [Mona Sans and Hubot Sans](https://github.com/mona-sans).
|
- The colour palette of both the light theme and the dark theme is from
|
||||||
- The monospace fonts are from the [Monaspace](https://monaspace.githubnext.com) font superfamily.
|
[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.
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
export const title = "Blog";
|
export const title = "Blog";
|
||||||
export const description = "Hello, stranger. Stay a while and listen.";
|
export const description = "Hello, stranger. Stay a while and listen.";
|
||||||
|
|
||||||
export default function({ nav, comp }) {
|
export default function ({ nav, comp }) {
|
||||||
const { PostListItem } = comp;
|
const { PostListItem } = comp;
|
||||||
|
|
||||||
const sortPosts = (a,b) => {
|
const sortPosts = (a, b) => {
|
||||||
if (a.data.date < b.data.date) {
|
if (a.data.date < b.data.date) {
|
||||||
return 1;
|
return 1;
|
||||||
} else if (a.data.date > b.data.date) {
|
} else if (a.data.date > b.data.date) {
|
||||||
|
@ -17,16 +17,24 @@ export default function({ nav, comp }) {
|
||||||
if (!nav.menu("/blog/posts")) {
|
if (!nav.menu("/blog/posts")) {
|
||||||
return (
|
return (
|
||||||
<div className="no-posts">
|
<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>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="post-list">
|
<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>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,35 +6,61 @@
|
||||||
- design
|
- 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
|
## 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
|
## 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.
|
> Dracula Pro is a paid theme and its license must be purchased. {.info}
|
||||||
{.info}
|
|
||||||
|
|
||||||
## 2: Catppuccin
|
## 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
|
## 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.
|
- **Night** - The main, dark variant.
|
||||||
- **Storm** - The same palette as Night except for a lighter background colour.
|
- **Storm** - The same palette as Night except for a lighter background colour.
|
||||||
- **Light** - A light variant for those who prefer a light theme.
|
- **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.
|
||||||
|
|
|
@ -7,33 +7,53 @@
|
||||||
- guide
|
- 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).
|
> If you get stuck, the best place to get help is by visiting the
|
||||||
{.info}
|
> [RetroPie forum](https://retropie.org.uk/forum). {.info}
|
||||||
|
|
||||||
## What is RetroPie?
|
## 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
|
## 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.
|
- Laptop with [Debian 12.2](https://www.debian.org/News/2023/20231007)
|
||||||
|
installed. Any [Debian](https://www.debian.org)-based distribution such as
|
||||||
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).
|
[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.
|
- Familiarity with Linux and its command line interface.
|
||||||
- Internet connection.
|
- Internet connection.
|
||||||
- A gaming controller. You can connect more after the installation.
|
- 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
|
- 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
|
## RetroPie Setup
|
||||||
|
|
||||||
|
@ -41,13 +61,13 @@ ### Setup Debian
|
||||||
|
|
||||||
Update the system and packages:
|
Update the system and packages:
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
sudo apt update && sudo apt upgrade
|
sudo apt update && sudo apt upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
Install RetroPie dependencies:
|
Install RetroPie dependencies:
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
sudo apt install git dialog unzip xmlstarlet
|
sudo apt install git dialog unzip xmlstarlet
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -55,19 +75,19 @@ ### Install RetroPie
|
||||||
|
|
||||||
Download the latest RetroPie script:
|
Download the latest RetroPie script:
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
git clone --depth=1 https://github.com/RetroPie/RetroPie-Setup.git
|
git clone --depth=1 https://github.com/RetroPie/RetroPie-Setup.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Enter the RetroPie directory:
|
Enter the RetroPie directory:
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
cd RetroPie-Setup
|
cd RetroPie-Setup
|
||||||
```
|
```
|
||||||
|
|
||||||
Execute the script:
|
Execute the script:
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
sudo ./retropie_setup.sh
|
sudo ./retropie_setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -75,25 +95,33 @@ ### Install RetroPie
|
||||||
|
|
||||||
![Screenshot of installation main menu](021af5d6-cc50-4ac7-8734-750d2a2e3789.png)
|
![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.
|
> The installation process compiles most of the emulators from source. This can
|
||||||
{.info}
|
> 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
|
### 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/
|
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.
|
> The default Gameboy Advance emulator, **lr-mgba**, accepts ROM files in the
|
||||||
{.info}
|
> 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
|
## RetroPie First Startup
|
||||||
|
|
||||||
|
@ -103,30 +131,42 @@ ### Start RetroPie
|
||||||
|
|
||||||
![RetroPie on the GNOME app dashboard](be60a03a-aa41-48f9-b02f-5c8c728e66f6.png)
|
![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.
|
> Alternatively, if you don't have a controller, you can press and hold the
|
||||||
{.info}
|
> <kbd>A</kbd> key on your keyboard to configure your keyboard as your
|
||||||
|
> controller. {.info}
|
||||||
|
|
||||||
![RetroPie controller setup](706ae312-eecc-470b-8b3a-429c220962af.png)
|
![RetroPie controller setup](706ae312-eecc-470b-8b3a-429c220962af.png)
|
||||||
|
|
||||||
### Launch a Game
|
### 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)
|
![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)
|
![Gameboy Advance game selection screen](6b9c2f7a-773d-4bac-a6f8-69668fba80dc.png)
|
||||||
|
|
||||||
## Conclusion
|
## 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.
|
[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:
|
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.
|
- If your laptop has an HDMI or DisplayPort port, connect it to a TV or an
|
||||||
- Add your installed PC games from [Steam](https://store.steampowered.com) or [GoG](https://www.gog.com).
|
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.
|
- Setup Debian to
|
||||||
- Use the [scraper](https://retropie.org.uk/docs/Scraper/?h=scraper) to populate the meta information and artwork for your games.
|
[auto login at boot](https://retropie.org.uk/docs/Debian/?h=autologin#ubuntu-does-not-autologin)
|
||||||
- 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.
|
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.
|
- Install more ROMs and build your retro game collection.
|
||||||
- Connect more controllers for multiplayer.
|
- Connect more controllers for multiplayer.
|
||||||
|
|
||||||
## Troubleshooting
|
## 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
|
### 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)
|
![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)
|
![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)
|
![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)
|
![RetroPie fullscreen no cut-off](14701639-0179-4d2c-934f-ad3a63e6f85e.png)
|
||||||
|
|
|
@ -7,46 +7,79 @@
|
||||||
- automation
|
- 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:
|
So, the options we have thus far require us to either:
|
||||||
|
|
||||||
- Pay for a more expensive internet plan.
|
- 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...
|
If only there was another way...
|
||||||
|
|
||||||
## The Solution
|
## 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. Obtain the current public IP address and the domain's A-record.
|
||||||
1. Compare the public IP and the A-record's value.
|
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:
|
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.
|
- Does not depend on a DDNS provider.
|
||||||
- No need for a router with DDNS support.
|
- 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
|
## Requirements
|
||||||
|
|
||||||
- Basic proficiency with Linux and its command line.
|
- 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.
|
- 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
|
## Preparation
|
||||||
|
|
||||||
|
@ -58,42 +91,48 @@ #### Update the System
|
||||||
|
|
||||||
Open a terminal then enter the command below to 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
|
sudo apt -y update && sudo apt -y upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Install Cron
|
#### 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
|
sudo apt install cron
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Install Deno
|
#### 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).
|
> Refer to the Deno documentation for
|
||||||
{.info}
|
> [additional installation options](https://docs.deno.com/runtime/manual/getting_started/installation).
|
||||||
|
> {.info}
|
||||||
|
|
||||||
Run the following command to install Deno:
|
Run the following command to install Deno:
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
curl -fsSL https://deno.land/x/install/install.sh | sh
|
curl -fsSL https://deno.land/x/install/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Confirm that Deno is properly installed by checking its version:
|
Confirm that Deno is properly installed by checking its version:
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
deno --version
|
deno --version
|
||||||
```
|
```
|
||||||
|
|
||||||
An output like the one below indicates that Deno was successfully installed:
|
An output like the one below indicates that Deno was successfully installed:
|
||||||
|
|
||||||
``` txt
|
```txt
|
||||||
deno 1.38.3 (release, x86_64-unknown-linux-gnu)
|
deno 1.38.3 (release, x86_64-unknown-linux-gnu)
|
||||||
v8 12.0.267.1
|
v8 12.0.267.1
|
||||||
typescript 5.2.2
|
typescript 5.2.2
|
||||||
|
@ -101,10 +140,12 @@ #### Install Deno
|
||||||
|
|
||||||
### GoDaddy API Keys
|
### 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.
|
> Ensure that you get the API secret and key for the **production environment**;
|
||||||
{.warning}
|
> not the ones for the test environment. {.warning}
|
||||||
|
|
||||||
## The Script
|
## The Script
|
||||||
|
|
||||||
|
@ -113,16 +154,18 @@ ## The Script
|
||||||
> - [GET /v1/domains/{domain}/records/{type}/{name}](https://developer.godaddy.com/doc/endpoint/domains#/v1/recordGet)
|
> - [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)
|
> - [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.
|
> Refer to the
|
||||||
{.info}
|
> [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
|
||||||
> 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.
|
> [IpMe](https://code.fosterhangdaan.com/foster/ipme) at
|
||||||
{.info}
|
> [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:
|
Here is the content of the script:
|
||||||
|
|
||||||
``` ts
|
```ts
|
||||||
#!/usr/bin/env -S deno run --allow-net
|
#!/usr/bin/env -S deno run --allow-net
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -148,15 +191,16 @@ ## The Script
|
||||||
|
|
||||||
const domainParts = domain.split(".");
|
const domainParts = domain.split(".");
|
||||||
const domainRoot = domainParts.slice(-2).join(".");
|
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({
|
const goDaddyRequestHeaders = new Headers({
|
||||||
Authorization: `sso-key ${GODADDY_API_KEY}:${GODADDY_API_SECRET}`,
|
Authorization: `sso-key ${GODADDY_API_KEY}:${GODADDY_API_SECRET}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [ publicIpResponse, goDaddyResponse ] = await Promise.all([
|
const [publicIpResponse, goDaddyResponse] = await Promise.all([
|
||||||
fetch(publicIpUrl),
|
fetch(publicIpUrl),
|
||||||
fetch(goDaddyUrl, { headers: goDaddyRequestHeaders }),
|
fetch(goDaddyUrl, { headers: goDaddyRequestHeaders }),
|
||||||
]);
|
]);
|
||||||
|
@ -177,7 +221,9 @@ ## The Script
|
||||||
const goDaddyIP = goDaddyJsonResponse[0]["data"];
|
const goDaddyIP = goDaddyJsonResponse[0]["data"];
|
||||||
|
|
||||||
if (publicIP !== goDaddyIP) {
|
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");
|
goDaddyRequestHeaders.append("Content-Type", "application/json");
|
||||||
const response = await fetch(goDaddyUrl, {
|
const response = await fetch(goDaddyUrl, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
@ -185,7 +231,7 @@ ## The Script
|
||||||
body: JSON.stringify([
|
body: JSON.stringify([
|
||||||
{
|
{
|
||||||
data: publicIP,
|
data: publicIP,
|
||||||
}
|
},
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
@ -198,28 +244,37 @@ ## The Script
|
||||||
|
|
||||||
## Schedule the Script to Run Every Hour with Cron
|
## 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 `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:
|
Grant read, write, and execute permissions only to the `root` user:
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
sudo chown root:root /etc/cron.hourly/ddns.ts
|
sudo chown root:root /etc/cron.hourly/ddns.ts
|
||||||
sudo chmod 700 /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.
|
> Access to the script should be granted only to authorized users (such as
|
||||||
{.warning}
|
> `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.
|
The script should now be scheduled to run every hour by Cron.
|
||||||
|
|
||||||
## Conclusion
|
## 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.
|
||||||
|
|
24
gpg-key.md
24
gpg-key.md
|
@ -3,20 +3,21 @@
|
||||||
description: Information about my GPG public key and how to obtain it.
|
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.
|
> I do not use keyservers. A key claiming to be mine from a keyserver is
|
||||||
{.warning}
|
> definitely a fake. {.warning}
|
||||||
|
|
||||||
> I suggest reading up on GPG if you are unfamiliar with it.
|
> I suggest reading up on GPG if you are unfamiliar with it. You can find
|
||||||
> You can find information about GPG on the [GnuPG official website](https://gnupg.org/).
|
> information about GPG on the [GnuPG official website](https://gnupg.org/).
|
||||||
{.info}
|
> {.info}
|
||||||
|
|
||||||
## Obtaining My Key
|
## 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:
|
The key's fingerprint should match the one below:
|
||||||
|
|
||||||
``` txt
|
```txt
|
||||||
pub ed25519/E48D7F49A852F112 2023-07-14 [SC]
|
pub ed25519/E48D7F49A852F112 2023-07-14 [SC]
|
||||||
Key fingerprint = DBD3 8E38 4B9E 1F4F 19F9 5BAE E48D 7F49 A852 F112
|
Key fingerprint = DBD3 8E38 4B9E 1F4F 19F9 5BAE E48D 7F49 A852 F112
|
||||||
uid Foster Hangdaan <foster@hangdaan.email>
|
uid Foster Hangdaan <foster@hangdaan.email>
|
||||||
|
@ -24,6 +25,11 @@ ## Obtaining My Key
|
||||||
|
|
||||||
## Utilizing 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.
|
||||||
|
|
68
index.tsx
68
index.tsx
|
@ -1,7 +1,7 @@
|
||||||
export const description = "Software developer and open-source enthusiast.";
|
export const description = "Software developer and open-source enthusiast.";
|
||||||
|
|
||||||
export default function({ nav }) {
|
export default function ({ nav }) {
|
||||||
const sortPosts = (a,b) => {
|
const sortPosts = (a, b) => {
|
||||||
if (a.data.date < b.data.date) {
|
if (a.data.date < b.data.date) {
|
||||||
return 1;
|
return 1;
|
||||||
} else if (a.data.date > b.data.date) {
|
} else if (a.data.date > b.data.date) {
|
||||||
|
@ -14,51 +14,85 @@ export default function({ nav }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>
|
<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>
|
||||||
<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.
|
You'll most likely find me within{" "}
|
||||||
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.
|
<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>
|
</p>
|
||||||
<h2 id="contact-me" tabIndex="-1">
|
<h2 id="contact-me" tabIndex="-1">
|
||||||
<a className="header-anchor" href="#contact-me">Contact Me</a>
|
<a className="header-anchor" href="#contact-me">Contact Me</a>
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<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>
|
||||||
<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>
|
</p>
|
||||||
<h2 id="highlighted-projects" tabIndex="-1">
|
<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>
|
</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://code.fosterhangdaan.com/foster/website">This website</a> — My personal website & blog.
|
<a href="https://code.fosterhangdaan.com/foster/website">
|
||||||
|
This website
|
||||||
|
</a>{" "}
|
||||||
|
— My personal website & blog.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://code.fosterhangdaan.com/foster/grub-themes">GRUB Themes</a> — 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>{" "}
|
||||||
|
— Collection of themes for the <strong>GR</strong>and{" "}
|
||||||
|
<strong>U</strong>nified <strong>B</strong>ootloader.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://code.fosterhangdaan.com/foster/ipme">IpMe</a> — A self-hostable API for obtaining your public IP address.
|
<a href="https://code.fosterhangdaan.com/foster/ipme">IpMe</a>{" "}
|
||||||
|
— A self-hostable API for obtaining your public IP address.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://code.fosterhangdaan.com/foster/bitcoin-core-container">Bitcoin Core Container</a> — A containerized Bitcoin node.
|
<a href="https://code.fosterhangdaan.com/foster/bitcoin-core-container">
|
||||||
|
Bitcoin Core Container
|
||||||
|
</a>{" "}
|
||||||
|
— A containerized Bitcoin node.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://code.fosterhangdaan.com/foster/monero-node-container">Monero Node Container</a> — A containerized Monero node.
|
<a href="https://code.fosterhangdaan.com/foster/monero-node-container">
|
||||||
|
Monero Node Container
|
||||||
|
</a>{" "}
|
||||||
|
— A containerized Monero node.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
<h2 id="latest-blog-posts" tabIndex="-1">
|
<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>
|
</h2>
|
||||||
<ul>
|
<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}>
|
<li key={index}>
|
||||||
<a href={post.data.url} >{post.data.title}</a> — <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> —{" "}
|
||||||
|
<time className="post-list-date">
|
||||||
|
{Intl.DateTimeFormat("en-CA", { dateStyle: "long" }).format(
|
||||||
|
post.data.date,
|
||||||
|
)}
|
||||||
|
</time>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
21
resume.md
21
resume.md
|
@ -7,33 +7,32 @@ ## Experience
|
||||||
|
|
||||||
### Startup Tech Pros
|
### Startup Tech Pros
|
||||||
|
|
||||||
Tech Lead<br/>
|
Tech Lead<br/> June 2022 - present
|
||||||
June 2022 - present
|
|
||||||
|
|
||||||
- Set standards and procedures for Engineering department.
|
- Set standards and procedures for Engineering department.
|
||||||
- Design, maintain and deploy cloud infrastructure.
|
- Design, maintain and deploy cloud infrastructure.
|
||||||
- Manage software development projects.
|
- Manage software development projects.
|
||||||
- Determine software tools and services based on company budget and requirements.
|
- Determine software tools and services based on company budget and
|
||||||
- Collaborate with clients to create websites and infrastructure based on business needs.
|
requirements.
|
||||||
|
- Collaborate with clients to create websites and infrastructure based on
|
||||||
|
business needs.
|
||||||
|
|
||||||
### Zetane Systems
|
### Zetane Systems
|
||||||
|
|
||||||
Full-Stack Web Developer<br/>
|
Full-Stack Web Developer<br/> October 2021 - December 2023
|
||||||
October 2021 - December 2023
|
|
||||||
|
|
||||||
- Develop pages and components in Next.js according to Figma designs.
|
- Develop pages and components in Next.js according to Figma designs.
|
||||||
- Create CI/CD pipeline to package application and deploy to AWS.
|
- 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.
|
- Refactored code and design of company website.
|
||||||
|
|
||||||
## Education
|
## Education
|
||||||
|
|
||||||
### Computer Engineering Technology
|
### Computer Engineering Technology
|
||||||
|
|
||||||
Emphasis in Computer Programming<br/>
|
Emphasis in Computer Programming<br/> Seneca College of Applied Arts and
|
||||||
Seneca College of Applied Arts and Technology<br/>
|
Technology<br/> Graduated August 2020<br/> Cumulative GPA 4.0/4.0<br/>
|
||||||
Graduated August 2020<br/>
|
|
||||||
Cumulative GPA 4.0/4.0<br/>
|
|
||||||
|
|
||||||
## Skills
|
## Skills
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue