Compare commits

..

No commits in common. "ea5052f04aa2f127ca0c9b906da1fb348ad7102d" and "be2e215b398a2a9cdde63fec2bd0049b94775897" have entirely different histories.

218 changed files with 16940 additions and 4786 deletions

View file

@ -0,0 +1,32 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/alpine
{
"name": "Hugo",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "klakegg/hugo:ext-alpine",
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [1313],
"onCreateCommand": {
"lfs": "apk add git-lfs",
"pagefind": "export VER=v1.0.3 && wget \"https://github.com/CloudCannon/pagefind/releases/download/${VER}/pagefind-${VER}-x86_64-unknown-linux-musl.tar.gz\" && tar -xf \"pagefind-${VER}-x86_64-unknown-linux-musl.tar.gz\" && mv pagefind /usr/bin/ && rm \"pagefind-${VER}-x86_64-unknown-linux-musl.tar.gz\""
},
"customizations": {
"vscode": {
"extensions": ["streetsidesoftware.code-spell-checker"]
}
}
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "uname -a",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View file

@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View file

@ -1,30 +0,0 @@
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};

7
.gitattributes vendored
View file

@ -1,7 +1,6 @@
*.png filter=lfs diff=lfs merge=lfs -text
*.pdf filter=lfs diff=lfs merge=lfs -text
*.webp filter=lfs diff=lfs merge=lfs -text
*.avif filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.svg filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text

12
.gitignore vendored
View file

@ -1,10 +1,2 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
public
.hugo_build.lock

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "themes/catafalque"]
path = themes/catafalque
url = https://github.com/SeriousBug/hugo-theme-catafalque.git

1
.npmrc
View file

@ -1 +0,0 @@
engine-strict=true

1
.nvmrc
View file

@ -1 +0,0 @@
v22.1.0

View file

@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View file

@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

5
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"cSpell.words": ["Actix", "Gandi"],
"cSpell.enabled": true,
"cSpell.enableFiletypes": ["markdown"]
}

31
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,31 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "dev",
"type": "shell",
"command": "hugo server -D",
"problemMatcher": []
},
{
"label": "build",
"type": "shell",
"command": "hugo",
"problemMatcher": []
},
{
"label": "pagefind",
"type": "shell",
"command": "pagefind --site public",
"problemMatcher": []
},
{
"label": "view",
"type": "shell",
"command": "python3 -m http.server 1313 --directory public",
"problemMatcher": []
}
]
}

View file

@ -1,16 +1,21 @@
clone:
git:
image: woodpeckerci/plugin-git
settings:
recursive: true
steps:
build:
image: node:22
image: klakegg/hugo:ext-alpine
commands:
- npm ci
- npm run build
- hugo
search:
image: seriousbug/pagefind
commands:
- pagefind --site public
deploy:
image: minio/mc
secrets: [MINIO_URL, ACCESS_KEY, SECRET_KEY]
commands:
- mc alias set minio $MINIO_URL $ACCESS_KEY $SECRET_KEY
- mc mirror --overwrite build/ minio/bgenc.net/
- mc mirror --overwrite public/ minio/bgenc.net/

View file

@ -1,38 +0,0 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

5
archetypes/default.md Normal file
View file

@ -0,0 +1,5 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

217
config.toml Normal file
View file

@ -0,0 +1,217 @@
baseURL = "/"
title = "Kaan Barmore-Genç"
languageCode = "en-us"
theme = "catafalque"
PygmentsCodeFences = true
PygmentsStyle = "monokai"
paginate = 20
rssLimit = 60 # Maximum number of items in the RSS feed.
copyright = "Contents are licensed under <a rel='license' href='http://creativecommons.org/licenses/by/4.0/'>CC 4.0</a> unless specified otherwise.<br />Source code for this website is available at <a href='https://gitea.bgenc.net/kaan/bgenc.net'>gitea.bgenc.net</a>."
# googleAnalytics = ""
# disqusShortname = ""
archetypeDir = "archetypes"
contentDir = "content"
dataDir = "data"
layoutDir = "layouts"
publishDir = "public"
buildDrafts = false
buildFuture = false
buildExpired = false
canonifyURLs = true
enableRobotsTXT = true
enableGitInfo = false
enableEmoji = true
enableMissingTranslationPlaceholders = false
disableRSS = false
disableSitemap = false
disable404 = false
disableHugoGeneratorInject = false
[permalinks]
posts = "/:filename/"
[author]
name = "Kaan Barmore-Genç"
[blackfriday]
hrefTargetBlank = true
#[taxonomies]
# tag = "tags"
# category = "categories"
# series = "series"
[params]
dateform = "Jan 2, 2006"
dateformShort = "Jan 2"
dateformNum = "2006-01-02"
dateformNumTime = "2006-01-02 15:04"
# Metadata mostly used in document's head
#
description = "Website of Kaan Barmore-Genç, and his personal blog"
keywords = ""
images = [""]
# Home subtitle of the index page.
#
homeSubtitle = [
"Hi! I'm a Software Engineer, an avid Linux user, an enthusiast of many programming languages, a <a href='/recipes'>home cook</a>, and an amateur gardener.",
"My interests include building web and mobile applications, both front and back end. Over the years I learned and used many programming languages and technologies, including JavaScript, TypeScript, React, React Native, Python, Java, C, C++, Clojure, Rust, and Haskell. Pretty much everything I've worked on is open source and available on <a href='https://github.com/SeriousBug/'>my Github page</a>.",
]
enablePagefind = true
# Set a background for the homepage
# backgroundImage = "assets/images/background.jpg"
# Prefix of link to the git commit detail page. GitInfo must be enabled.
#
# gitUrl = ""
# Set disableReadOtherPosts to true in order to hide the links to other posts.
#
disableReadOtherPosts = false
# Enable theme toggle
#
# This options enables the theme toggle for the theme.
# Per default, this option is off.
# The theme is respecting the prefers-color-scheme of the operating systeme.
# With this option on, the page user is able to set the scheme he wants.
enableThemeToggle = true
# Sharing buttons
#
# There are a lot of buttons preconfigured. If you want to change them,
# generate the buttons here: https://sharingbuttons.io
# and add them into your own `layouts/partials/sharing-buttons.html`
#
enableSharingButtons = false
# Global language menu
#
# Enables the global language menu.
#
enableGlobalLanguageMenu = false
# Integrate Javascript files or stylesheets by adding the url to the external assets or by
# linking local files with their path relative to the static folder, e.g. "css/styles.css"
#
customCSS = []
customJS = []
# Toggle this option need to rebuild SCSS, requires extended version of Hugo
#
justifyContent = false # Set "text-align: justify" to .post-content.
# Custom footer
# If you want, you can easily override the default footer with your own content.
#
[params.footer]
trademark = false
rss = true
copyright = true
author = false
bottomText = [
"This website is available at <a href='https://bgenc.net'>bgenc.net</a>, or as an onion service at <a href='http://bgenc2iv62mumkhu2p564vxtao6ha7ihavmzwpetkmazgq6av7zvfwyd.onion/'>bgenc2iv62mumkhu2p564vxtao6ha7ihavmzwpetkmazgq6av7zvfwyd.onion</a> which you can view through the <a href='https://www.torproject.org/download/' target='_blank'>Tor browser</a>.",
]
topText = []
# bottomText = [
# "Powered by <a href=\"http://gohugo.io\">Hugo</a>",
# "Made with &#10084; by <a href=\"https://github.com/rhazdon\">Djordje Atlialp</a>",
# ]
# Colors for favicons
#
[params.favicon.color]
mask = "#1b1c1d"
msapplication = "#1b1c1d"
theme = "#1b1c1d"
[params.logo]
logoMark = ">"
logoText = "Kaan Barmore-Genç"
logoHomeLink = "/"
# Set true to remove the logo cursor entirely.
# logoCursorDisabled = false
# Set to a valid CSS color to change the cursor in the logo.
# logoCursorColor = "#67a2c9"
# Set to a valid CSS time value to change the animation duration, "0s" to disable.
# logoCursorAnimate = "2s"
# Commento is more than just a comments widget you can embed —
# its a return to the roots of the internet.
# An internet without the tracking and invasions of privacy.
# An internet that is simple and lightweight.
# An internet that is focused on interesting discussions, not ads.
# A better internet.
# Uncomment this to enable Commento.
#
# [params.commento]
# url = ""
[params.portrait]
path = "/img/profile.2022.12.jpeg"
pathWebp = "/img/profile.2022.12.webp"
pathAvif = "/img/profile.2022.12.avif"
alt = "A picture of Kaan, wearing a beanie, in front of some shrubbery."
maxWidth = "20rem"
# Social icons
[[params.social]]
name = "email"
url = "mailto:kaan@bgenc.net"
me = true
[[params.social]]
name = "github"
url = "https://github.com/SeriousBug/"
me = true
[[params.social]]
name = "mastodon"
url = "https://fosstodon.org/@kaan"
me = true
[[params.social]]
name = "linkedin"
url = "https://www.linkedin.com/in/kaan-barmore-genc"
[[params.social]]
name = "cv"
title = "CV"
url = "/extra/cv.pdf"
# [languages]
# [languages.en]
# subtitle = "Hello Friend NG Theme"
# weight = 1
# copyright = '<a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY-NC 4.0</a>'
# [languages.fr]
# subtitle = "Hello Friend NG Theme"
# weight = 2
# copyright = '<a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY-NC 4.0</a>'
[menu]
#[[menu.main]]
#identifier = "about"
#name = "About"
#url = "about/"
[[menu.main]]
identifier = "posts"
name = "Blog"
url = "posts/"
[[menu.main]]
identifier = "portfolio"
name = "Portfolio"
url = "portfolio/"

BIN
content/apple-touch-icon.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
content/extra/cv.pdf Executable file

Binary file not shown.

70
content/extra/emacs.css Normal file
View file

@ -0,0 +1,70 @@
/* pygments.org "emacs" style */
.highlight .hll { background-color: #ffffcc }
.highlight { background: #f8f8f8; }
.highlight .c { color: #008800; font-style: italic } /* Comment */
.highlight .err { border: 1px solid #FF0000 } /* Error */
.highlight .k { color: #AA22FF; font-weight: bold } /* Keyword */
.highlight .o { color: #666666 } /* Operator */
.highlight .ch { color: #008800; font-style: italic } /* Comment.Hashbang */
.highlight .cm { color: #008800; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #008800 } /* Comment.Preproc */
.highlight .cpf { color: #008800; font-style: italic } /* Comment.PreprocFile */
.highlight .c1 { color: #008800; font-style: italic } /* Comment.Single */
.highlight .cs { color: #008800; font-weight: bold } /* Comment.Special */
.highlight .gd { color: #A00000 } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #FF0000 } /* Generic.Error */
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.highlight .gi { color: #00A000 } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.highlight .gt { color: #0044DD } /* Generic.Traceback */
.highlight .kc { color: #AA22FF; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #AA22FF; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #AA22FF; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #AA22FF } /* Keyword.Pseudo */
.highlight .kr { color: #AA22FF; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #00BB00; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #666666 } /* Literal.Number */
.highlight .s { color: #BB4444 } /* Literal.String */
.highlight .na { color: #BB4444 } /* Name.Attribute */
.highlight .nb { color: #AA22FF } /* Name.Builtin */
.highlight .nc { color: #0000FF } /* Name.Class */
.highlight .no { color: #880000 } /* Name.Constant */
.highlight .nd { color: #AA22FF } /* Name.Decorator */
.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */
.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #00A000 } /* Name.Function */
.highlight .nl { color: #A0A000 } /* Name.Label */
.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #B8860B } /* Name.Variable */
.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #666666 } /* Literal.Number.Bin */
.highlight .mf { color: #666666 } /* Literal.Number.Float */
.highlight .mh { color: #666666 } /* Literal.Number.Hex */
.highlight .mi { color: #666666 } /* Literal.Number.Integer */
.highlight .mo { color: #666666 } /* Literal.Number.Oct */
.highlight .sa { color: #BB4444 } /* Literal.String.Affix */
.highlight .sb { color: #BB4444 } /* Literal.String.Backtick */
.highlight .sc { color: #BB4444 } /* Literal.String.Char */
.highlight .dl { color: #BB4444 } /* Literal.String.Delimiter */
.highlight .sd { color: #BB4444; font-style: italic } /* Literal.String.Doc */
.highlight .s2 { color: #BB4444 } /* Literal.String.Double */
.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
.highlight .sh { color: #BB4444 } /* Literal.String.Heredoc */
.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
.highlight .sx { color: #008000 } /* Literal.String.Other */
.highlight .sr { color: #BB6688 } /* Literal.String.Regex */
.highlight .s1 { color: #BB4444 } /* Literal.String.Single */
.highlight .ss { color: #B8860B } /* Literal.String.Symbol */
.highlight .bp { color: #AA22FF } /* Name.Builtin.Pseudo */
.highlight .fm { color: #00A000 } /* Name.Function.Magic */
.highlight .vc { color: #B8860B } /* Name.Variable.Class */
.highlight .vg { color: #B8860B } /* Name.Variable.Global */
.highlight .vi { color: #B8860B } /* Name.Variable.Instance */
.highlight .vm { color: #B8860B } /* Name.Variable.Magic */
.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */

View file

@ -0,0 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
2Vw6bCeYRslIgNo=
=tme2
-----END PGP PUBLIC KEY BLOCK-----

BIN
content/favicon-16x16.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
content/favicon-32x32.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
content/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

57
content/portfolio.md Normal file
View file

@ -0,0 +1,57 @@
This page lists the projects that I'm most proud of, and that I'm actively developing and supporting.
### [Gandi Live Dns Rust ![GitHub Repo stars](https://img.shields.io/github/stars/SeriousBug/gandi-live-dns-rust) ![Docker Pulls](https://img.shields.io/docker/pulls/seriousbug/gandi-live-dns-rust)](https://github.com/SeriousBug/gandi-live-dns-rust)
A dynamic DNS system that works with Gandi's live DNS feature. Allows you to
host servers without a static IP address by updating DNS records whenever your
IP changes. Flexible deployments through Docker or system packages with systemd
timers.
### [Cuttlestore ![GitHub Repo stars](https://img.shields.io/github/stars/SeriousBug/cuttlestore) ![Crates.io](https://img.shields.io/crates/d/cuttlestore)](https://github.com/SeriousBug/cuttlestore)
A generic key-value storage library for Rust. Cuttlestore allows you to write
your code once and run it on many key-value stores. Right now it comes with
support for Redis and Sqlite, with planned support for CouchDB and DynamoDB.
### [Rust Embed for Web ![GitHub Repo stars](https://img.shields.io/github/stars/SeriousBug/rust-embed-for-web) ![Crates.io](https://img.shields.io/crates/d/rust-embed-for-web)](https://github.com/SeriousBug/rust-embed-for-web)
Embed files into your Rust executable. You can embed HTML, CSS, JavaScript
files, all your assets into your server to bundle them together. This simplifies
updates as your assets are always guaranteed to update together with your server.
This started as a fork of an existing project, but became a significant rewrite
of it. It includes many features useful for web servers like precomputed header
values and precompressed file contents.
### [Rust Embed Responder for Actix Web ![GitHub Repo stars](https://img.shields.io/github/stars/SeriousBug/actix-web-rust-embed-responder) ![Crates.io](https://img.shields.io/crates/d/actix-web-rust-embed-responder)](https://github.com/SeriousBug/actix-web-rust-embed-responder)
A sibling project to Rust Embed for Web, this is a responder for Actix Web that
efficiently serves your embedded files. It handles cache validation and content
type negotiation, and is built for high performance.
### [Bulgur Cloud ![GitHub Repo stars](https://img.shields.io/github/stars/bulgur-cloud/bulgur-cloud) ![Docker Pulls](https://img.shields.io/docker/pulls/seriousbug/bulgur-cloud)](https://github.com/bulgur-cloud/bulgur-cloud)
![](https://media.githubusercontent.com/media/bulgur-cloud/bulgur-cloud.github.io/main/static/img/homepage-screenshot.png)
An easy to self host cloud file storage and sharing system. It's similar to Google Drive or NextCloud, but effortless to set up and maintain. Built in Rust and TypeScript, using Actix-Web and React Native.
### [live limit ![GitHub Repo stars](https://img.shields.io/github/stars/SeriousBug/live-limit) ![npm](https://img.shields.io/npm/dt/live-limit)](https://github.com/SeriousBug/live-limit)
A TypeScript library that can limit the number of concurrent async operations
running at a time. This is useful for making concurrent requests to a server
without overloading your connection. Works with promises, has no dependencies,
and comes under 1kb minzipped.
### [Query Method Middleware for Actix Web ![GitHub Repo stars](https://img.shields.io/github/stars/SeriousBug/actix-web-query-method-middleware) ![Crates.io](https://img.shields.io/crates/d/actix-web-query-method-middleware)](https://github.com/SeriousBug/actix-web-query-method-middleware)
Actix Web middleware that allows you to submit HTML forms using methods other
than `POST`. Forms normally can only be submitted through `GET` or `POST`
methods, but this middleware reroutes requests using a query parameter to other
methods.
### [Http Drogue](https://github.com/SeriousBug/http-drogue) ![GitHub Repo stars](https://img.shields.io/github/stars/SeriousBug/http-drogue)
![](https://raw.githubusercontent.com/SeriousBug/http-drogue/main/pub/screenshot.png)
A tiny self-hosted service to download files over http, with support for resuming and restarting failed downloads.
Built with Rust, basic HTML templates using Askama, TailwindCSS, and DaisyUI. Uses no javascript!

View file

@ -1,5 +1,5 @@
---
title: 'Adding an HTML-only interface for Bulgur Cloud'
title: "Adding an HTML-only interface for Bulgur Cloud"
date: 2022-04-17T04:49:20-04:00
draft: true
toc: false
@ -36,7 +36,7 @@ and Actix will finish rendering and serving the page for me. Finally,
great because you can ship a single binary which includes all the files you
need!
![A web page with the name kaan and a link Logout at the top. Below is a list of files and folders. The bottom has some text noting Bulgur Cloud is open source, and that this is the HTML-only version.](/img/bulgur-cloud-basic-html.png)
![A web page with the name "kaan" and a link "Logout" at the top. Below is a list of files and folders. The bottom has some text noting Bulgur Cloud is open source, and that this is the HTML-only version.](/img/bulgur-cloud-basic-html.png)
It's all hand-written HTML and CSS. It was very quick to get all of this
working. Once the web app is done, I'll come back to this interface to add full

View file

@ -1,5 +1,5 @@
---
title: 'Solving React Redux Triggering Too Many Re-Renders'
title: "Solving React Redux Triggering Too Many Re-Renders"
date: 2022-09-18T18:13:31-04:00
toc: false
images:
@ -12,7 +12,7 @@ tags:
---
This might be obvious for some, but I was struggling with a performance issue in
[Bulgur Cloud](/portfolio), my React (well, React Native) based
[Bulgur Cloud](/portfolio/#bulgur-cloud), my React (well, React Native) based
web application. Bulgur Cloud is an app like Google Drive or NextCloud, and one
of the features is that you can upload files. But I noticed that the page would
slow down to a crawl and my computers fans would spin up during uploads. It
@ -24,10 +24,12 @@ browser and enabled the "Highlight updates when components render" option. Then
I used the network tab to add a throttle so I could see the upload happen more
slowly, and started another upload.
{{<raw>}}
<video controls width="100%">
<source src="/vid/react-redux-causes-re-renders.mp4" type="video/mp4">
<p>A video showing a progress bar slowly increasing. As the progress bar goes up, the entire screen flashes with blue borders.</p>
</video>
{{</raw>}}
Pretty much the entire screen flashes every time the progress bar goes up.
Something **is** causing unnecessary re-renders! The next step in my diagnosis
@ -86,22 +88,19 @@ comparison. I used a `shallowEquals` function to do a single-depth comparison of
the objects (the object is flat so I don't need recursion).
```ts
export function shallowEquals<
Left extends Record<string, unknown>,
Right extends Record<string, unknown>
>(left: Left, right: Right) {
if (Object.keys(left).length !== Object.keys(right).length) return false;
for (const key of Object.keys(left)) {
if (left[key] !== right[key]) return false;
}
return true;
export function shallowEquals<Left extends Record<string, unknown>, Right extends Record<string, unknown>>(left: Left, right: Right) {
if (Object.keys(left).length !== Object.keys(right).length) return false;
for (const key of Object.keys(left)) {
if (left[key] !== right[key]) return false;
}
return true;
}
// ...
const { access_token, site } = useAppSelector(
(selector) => pick(selector.auth, 'access_token', 'site'),
shallowEquals
);
const { access_token, site } = useAppSelector((selector) =>
pick(selector.auth, "access_token", "site"),
shallowEquals
);
```
Let's look at the profile now:

View file

@ -0,0 +1,47 @@
---
title: "Browser Caching: Assets not revalidated when server sends a 304 'Not Modified' for html page"
date: 2022-10-15T20:56:36-04:00
toc: false
images:
tags:
- dev
- web
---
I've been working on some web server middleware, and hit a weird issue that I
couldn't find documented anywhere. First, let's look at an overview of how
browser caching works:
If your web server sends an
[ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) header in
a HTTP response, the web browser may choose to cache the response. Next time the
same object is requested, the browser may add an
[If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match)
header to let the server know that the browser might have the object cached. At this point, the server should respond with the
[304 Not Modified](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304)
code and skip sending the response. This can also happen with the
[Last Modified](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified)
and
[If-Modified-Since](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since)
headers if `ETag` is not supported as well.
After implementing this in my middleware, I made a quick test website to try it
out. That's when I ran into a weird behavior: the browser would revalidate the
HTML page itself with the `If-None-Match` header, but when the server responded
with `304` it would not attempt to revalidate the linked stylesheets, scripts,
and images. The browser would not request them at all and immediately use the
cached version. It looks like if the server responds with `304` on the HTML
page, the browser assumes that all the linked assets are not modified as well.
That means that if the asset does change (you weren't using something like
fingerprinting or versioning on your assets), then the browser will use outdated
assets. Oops!
Luckily it looks like there's an easy solution: add `Cache-Control: no-cache`
header to your responses. `no-cache` doesn't actually mean "don't cache at all",
but rather means that the browser needs to revalidate objects before using
the cached version.
Without the `Cache-Control` header:
![Browser developer tools window, there is only 1 request for /](/img/browser-caching-before.png)
With the `Cache-Control` header:
![Browser developer tools window, there are 5 requests in total, including /, style.css, and 3 images.](/img/browser-caching-after.png)

View file

@ -5,13 +5,13 @@ toc: false
images:
tags:
- dev
- web
- web
---
I've been seeing this error a lot while working on my project [Bulgur Cloud](/bulgur-cloud-intro/).
It seems to show up only on Firefox:
![Error message in Firefox console. XML Parsing Error: no root element found. Location: http://...701.jpg Line Number 1, Column 1](/img/2022-12-17.firefox.xml-parsing-error.png)
![Error message in Firefox console. "XML Parsing Error: no root element found. Location: http://...701.jpg Line Number 1, Column 1](/img/2022-12-17.firefox.xml-parsing-error.png)
What is curious was that I was not actually loading the file mentioned in the
error message. I tried looking up what the error might mean, but all that came

View file

@ -1,5 +1,5 @@
---
title: 'Why I use Dev containers for most of my projects'
title: "Why I use Dev containers for most of my projects"
date: 2023-02-09T23:14:05-05:00
toc: false
images:
@ -44,7 +44,7 @@ stick with docker compose alone then!).
VSCode comes with commands to automatically generate a dev container
configuration for you by answering a few questions.
![A VSCode prompt window. deb is typed into the prompt, and the text Simple debian container with git installed is highlighted below.](/img/devcontainer-debian-example.png)
![A VSCode prompt window. "deb" is typed into the prompt, and the text "Simple debian container with git installed" is highlighted below.](/img/devcontainer-debian-example.png)
At the core of dev containers, what sets it apart from just using Docker is the
"features". These are pre-made recipes that install some tool or set up some
@ -55,13 +55,13 @@ install or set up anything else that features didn't cover.
```json
{
// ...
"features": {
"ghcr.io/devcontainers/features/node:1": {},
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}
},
"updateContentCommand": "pnpm install"
// ...
// ...
"features": {
"ghcr.io/devcontainers/features/node:1": {},
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}
},
"updateContentCommand": "pnpm install",
// ...
}
```
@ -119,38 +119,42 @@ Then, you just tell your dev container config to use the docker compose file.
```json
{
"name": "my-project",
"dockerComposeFile": "docker-compose.yml",
"service": "devcontainer",
"workspaceFolder": "/workspaces/my-project",
"name": "my-project",
"dockerComposeFile": "docker-compose.yml",
"service": "devcontainer",
"workspaceFolder": "/workspaces/my-project",
// Adding the Rust compiler, plus the AWS cli so I can access the S3 API of minio from the CLI.
"features": {
"ghcr.io/devcontainers/features/rust:1": {},
"ghcr.io/devcontainers/features/aws-cli:1": {}
},
// Adding the Rust compiler, plus the AWS cli so I can access the S3 API of minio from the CLI.
"features": {
"ghcr.io/devcontainers/features/rust:1": {},
"ghcr.io/devcontainers/features/aws-cli:1": {}
},
// The project I'm working on exposes the port 8080.
// I forward that out so I can look at it on my browser.
"forwardPorts": [8080],
// The project I'm working on exposes the port 8080.
// I forward that out so I can look at it on my browser.
"forwardPorts": [8080],
// Set up the development AWS config and credentials with test values,
"onCreateCommand": "mkdir -p ~/.aws/ && /bin/echo -e '[default]\nregion = local' > ~/.aws/config && /bin/echo -e '[default]\naws_access_key_id = minioadmin\naws_secret_access_key = minioadmin' > ~/.aws/credentials",
// Set up the development AWS config and credentials with test values,
"onCreateCommand": "mkdir -p ~/.aws/ && /bin/echo -e '[default]\nregion = local' > ~/.aws/config && /bin/echo -e '[default]\naws_access_key_id = minioadmin\naws_secret_access_key = minioadmin' > ~/.aws/credentials",
// Create the S3 bucket
"postCreateCommand": "aws s3 --endpoint-url $S3_ENDPOINT mb s3://my-bucket",
// Create the S3 bucket
"postCreateCommand": "aws s3 --endpoint-url $S3_ENDPOINT mb s3://my-bucket",
// I found that I have to add this, but it's not the default. Not sure why.
"remoteUser": "root",
// I found that I have to add this, but it's not the default. Not sure why.
"remoteUser": "root",
"customizations": {
// You can even add in VSCode extensions that everyone working on the project
// would need, without them having to install it on their own setup manually.
"vscode": {
"extensions": ["rust-lang.rust-analyzer", "streetsidesoftware.code-spell-checker"]
}
}
"customizations": {
// You can even add in VSCode extensions that everyone working on the project
// would need, without them having to install it on their own setup manually.
"vscode": {
"extensions": [
"rust-lang.rust-analyzer",
"streetsidesoftware.code-spell-checker"
]
}
}
}
```
That's it! Run the "Dev Containers: Reopen in Container" command in VSCode, give

View file

@ -70,17 +70,18 @@ side menu.
Scroll to the bottom, where you'll find "Required workflows". Click
to add a workflow.
![The required workflows section in Github organization settings. An Add workflow button is present.](/img/gh-required-workflows.png)
![The required workflows section in Github organization settings. An "Add workflow" button is present.](/img/gh-required-workflows.png)
Then select the repository where you added your action, and
write the path to the workflow file within that repository.
![Add required workflow page. The previously mentioned repository is selected, and the path do-not-merge.yml is written next to that. A selection below has picked 'All repositories'.](/img/gh-required-workflows-config.png)
You're now done! All PRs in all repositories will run the do not merge label
check, and will prevent you from merging any PR with the label.
![The checks section on a PR page. A check named Do Not Merge has failed, and the merge button is disabled. Github warns that all checks must pass before merging.](/img/gh-do-not-merge-fail.png)
![The checks section on a PR page. A check named "Do Not Merge" has failed, and the merge button is disabled. Github warns that all checks must pass before merging.](/img/gh-do-not-merge-fail.png)
One caveat is
that there seems to be a bug on Github's end of things where for any PR that was

View file

@ -1,5 +1,5 @@
---
title: 'Setting up my blog as an Onion service (Tor hidden service)'
title: "Setting up my blog as an Onion service (Tor hidden service)"
date: 2023-03-05T15:54:13-05:00
toc: false
images:
@ -12,7 +12,7 @@ As [online services are happy to turn over our data to the authorities](https://
it is crucial for Tor to exist so journalists, activists, whistle-blowers, and
anyone living under oppressive regimes can access information and communicate freely.
![A chart showing daily snowflake users in 2022. The numbers start to rise in December 2021, which is marked as Unblocking in Russia. The numbers then skyrocket in September, which is marked as Protests in Iran.](/img/tor-censorship-snowflake-chart.webp)
![A chart showing daily snowflake users in 2022. The numbers start to rise in December 2021, which is marked as "Unblocking in Russia". The numbers then skyrocket in September, which is marked as "Protests in Iran".](/img/tor-censorship-snowflake-chart.webp)
But there is really no reason for Tor to be used solely by people trying to
avoid censorship or stay private. In fact, I think it is good for people to use
@ -82,12 +82,12 @@ I finally added the tor container to a `docker-compose.yml` to make it easier to
rebuild if needed. That looks like this:
```yml
tor-hidden-service:
image: seriousbug/tor
restart: always
volumes:
- ./tor:/etc/tor
- /var/run/nginx:/var/run/tor
tor-hidden-service:
image: seriousbug/tor
restart: always
volumes:
- ./tor:/etc/tor
- /var/run/nginx:/var/run/tor
```
I also needed to make the tor directory with the configuration file and services
@ -95,3 +95,4 @@ owned by root, and use 700 as the file permission. Otherwise Tor refuses to star
Once all of this is set up, I restarted nginx and my Tor container. And that was about it!
The website is now accessible through Tor! You can find it at [bgenc2iv62mumkhu2p564vxtao6ha7ihavmzwpetkmazgq6av7zvfwyd.onion](http://bgenc2iv62mumkhu2p564vxtao6ha7ihavmzwpetkmazgq6av7zvfwyd.onion/).

View file

@ -1,5 +1,5 @@
---
title: 'Self Hosted Backups with Minio, Kopia, and Tailscale'
title: "Self Hosted Backups with Minio, Kopia, and Tailscale"
date: 2023-04-25T00:11:31-04:00
toc: false
images:
@ -19,7 +19,7 @@ limit them into specific folders too. But I kept hitting issues while setting th
refusing connections whenever the limit is active. While I'm sure there's an answer to why I was failing to set this up,
I came up with an easier solution: Minio.
Minio is an S3 compatible, self hosted block storage service.
Minio is an S3 compatible, self hosted block storage service.
It is generally meant to be used in clusters, but there's nothing to stop you from putting it on a single device!
You do lose a few features like file locking, but most features still work.
Kopia has S3 support, so it should work with Minio.
@ -34,8 +34,8 @@ minio:
volumes:
- minio-data:/data
ports:
- '9000:9000'
- '9001:9001'
- "9000:9000"
- "9001:9001"
env_file:
- .minio.env
```
@ -43,7 +43,7 @@ minio:
Then in `.minio.env`, I enter the root username and password:
```
MINIO_ROOT_USER=...
MINIO_ROOT_USER=...
MINIO_ROOT_PASSWORD=...
```
@ -67,7 +67,7 @@ to devices on your Tailscale network. And another feature allows you to generate
certificates for your MagicDNS domains. This is really cool because the generated certificates are "real",
they are not self-signed certificates and you don't need anything special for browsers and other tools to accept them.
![A web page with the contents: HTTPS Certificates. Beta. Allow users to provision HTTPS cerificates for their devices. Learn More. Below is a button labeled Disable HTTPS.](/img/2023-04-25.tailscale.png)
![A web page with the contents: HTTPS Certificates. Beta. Allow users to provision HTTPS cerificates for their devices. Learn More. Below is a button labeled "Disable HTTPS".](/img/2023-04-25.tailscale.png)
So putting these together, I enabled MagicDNS and HTTPS certificates for my network. Then,
I generated my certificates with `sudo tailscale cert --cert-file public.crt --key-file private.key hostname.network.ts.net`,
@ -83,8 +83,8 @@ minio:
- minio-data:/data
- ./certs:/certs
ports:
- '9000:9000'
- '9001:9001'
- "9000:9000"
- "9001:9001"
env_file:
- .minio.env
```
@ -104,19 +104,28 @@ backups bucket with this policy:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:*"],
"Resource": ["arn:aws:s3:::backup/*", "arn:aws:s3:::backup"]
},
{
"Effect": "Allow",
"Action": ["s3:ListAllMyBuckets"],
"Resource": ["arn:aws:s3:::*"]
}
]
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::backup/*",
"arn:aws:s3:::backup"
]
},
{
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets"
],
"Resource": [
"arn:aws:s3:::*"
]
}
]
}
```

View file

@ -1,5 +1,5 @@
---
title: 'CSS only placeholder for contenteditable elements'
title: "CSS only placeholder for contenteditable elements"
date: 2023-07-02T13:06:15-05:00
toc: false
images:
@ -31,23 +31,24 @@ way to add a placeholder to a `contenteditable` span without javascript. Here it
```html
<span contenteditable="true" data-placeholder="click on me and type"></span>
<style>
/* Add the placeholder text */
[data-placeholder]::before {
content: attr(data-placeholder);
/* Or whatever */
color: gray;
}
/* Hide the placeholder when selected, or when there is text inside */
[data-placeholder]:focus::before,
[data-placeholder]:not(:empty)::before {
content: none;
}
/* Add the placeholder text */
[data-placeholder]::before {
content: attr(data-placeholder);
/* Or whatever */
color: gray;
}
/* Hide the placeholder when selected, or when there is text inside */
[data-placeholder]:focus::before, [data-placeholder]:not(:empty)::before {
content: none;
}
</style>
```
And here what it looks like:
{{<raw>}}
<iframe width="100%" height="300" src="//jsfiddle.net/SeriousBug/t9hmgyq5/13/embedded/result,html,css/" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
{{</raw>}}
This also works with a `div`, but there is one caveat: If the user types
something into the `div` then deletes it, the browser will leave a `<br/>` in

View file

@ -1,5 +1,5 @@
---
title: 'Getting theme colors in JavaScript using React with DaisyUI and TailwindCSS'
title: "Getting theme colors in JavaScript using React with DaisyUI and TailwindCSS"
date: 2023-08-10T00:18:27-05:00
toc: false
images:
@ -23,7 +23,7 @@ this code:
```ts
function getPropertyValue(name: string) {
return getComputedStyle(document.body).getPropertyValue(name);
return getComputedStyle(document.body).getPropertyValue(name);
}
```
@ -36,10 +36,10 @@ primary and secondary colors, and the colors for the text that goes on top of
them.
```ts
const primary = getPropertyValue('--p');
const secondary = getPropertyValue('--s');
const primaryText = getPropertyValue('--pc');
const secondaryText = getPropertyValue('--sc');
const primary = getPropertyValue("--p");
const secondary = getPropertyValue("--s");
const primaryText = getPropertyValue("--pc");
const secondaryText = getPropertyValue("--sc");
```
This is all great! But I had one more concern: I needed a way to change these
@ -54,18 +54,18 @@ Here's how that code looks like:
```ts
export function useThemeColor() {
const themeFetcher = useCallback(() => {
const primary = getPropertyValue('--p');
const primaryText = getPropertyValue('--pc');
const secondary = getPropertyValue('--s');
const secondaryText = getPropertyValue('--sc');
return { primary, primaryText, secondary, secondaryText };
}, []);
const themeFetcher = useCallback(() => {
const primary = getPropertyValue("--p");
const primaryText = getPropertyValue("--pc");
const secondary = getPropertyValue("--s");
const secondaryText = getPropertyValue("--sc");
return { primary, primaryText, secondary, secondaryText };
}, []);
// The key "data:theme" could be anything, as long as it's unique in the app
const { data: color, mutate } = useSWR('data:theme', themeFetcher);
// The key "data:theme" could be anything, as long as it's unique in the app
const { data: color, mutate } = useSWR("data:theme", themeFetcher);
return { ...color, mutate };
return { ...color, mutate };
}
```
@ -98,7 +98,7 @@ export default function Dashboard() {
Here's what that looks like:
![A screenshot of a web page. At the top there is a dark red colored button labelled Dashboard. There is a line chart below, which uses the same dark red color as the button.](/img/2023-08-10.chartjs.png)
![A screenshot of a web page. At the top there is a dark red colored button labelled "Dashboard". There is a line chart below, which uses the same dark red color as the button.](/img/2023-08-10.chartjs.png)
To keep the colors changing whenever the user toggles the theme, you then just
have to call the `mutate` function inside your toggle button.

View file

@ -0,0 +1,180 @@
---
title: "Building a Well Organized Rust CLI Tool"
date: 2023-12-09T00:05:58-06:00
toc: false
images:
tags:
- dev
- rust
---
Here's a pattern came up with for building CLI tools in Rust using
[Clap](https://crates.io/crates/clap) with derive and using a custom trait.
First, define a "run command" trait that all commands will use.
```rs
#[async_trait::async_trait]
pub trait RunCommand {
async fn run(&self) -> anyhow::Result<()>;
}
```
I'm using [async-trait](https://crates.io/crates/async-trait) so my commands can
be async, and [anyhow](https://crates.io/crates/anyhow) for error handling
because I don't particularly care about the types of the errors my commands
might return. Neither of these are required though, skip them if you prefer.
The "trick" is to then implement your `RunCommand` trait for all the commands
and subcommands for your Clap parser. This is just how traits work, but what's
useful is that it makes it really easy to organize your commands and subcommands
into different folders while keeping the definition of the command next to the
implementation of that command.
For example, imagine a program with commands like:
```
myprogram query --some-params
myprogram account create --other-params
myprogram account delete --even-more-params
```
I'd recommend organizing the code into a folder structure like this:
```
.
├── account
│ ├── create.rs
│ ├── delete.rs
│ └── mod.rs
├── mod.rs
├── query.rs
└── run_command.rs <-- the RunCommand trait is defined here
```
These files will then look like this:
##### `account/create.rs`:
```rs
#[derive(Debug, Args)]
pub struct AccountCreate { /* params */ }
#[async_trait::async_trait]
impl RunCommand for AccountCreate {
async fn run(&self) -> anyhow::Result<()> {
// Your command implementation here!
}
}
```
##### `account/delete.rs`
Omitted for brevity, same as create
##### `account/mod.rs`:
```rs
#[derive(Debug, Subcommand)]
pub enum AccountCommands {
Create(AccountCreate),
Delete(AccountDelete),
}
#[async_trait]
impl RunCommand for AccountCommands {
async fn run(&self) -> anyhow::Result<()> {
match self {
Self::Create(cmd) => cmd.run().await,
Self::Delete(cmd) => cmd.run().await,
}
}
}
#[derive(Debug, Args)]
pub struct Account {
#[command(subcommand)]
command: AccountCommands,
}
#[async_trait]
impl RunCommand for Account {
async fn run(&self) -> anyhow::Result<()> {
self.command.run().await
}
}
```
##### `query.rs`:
```rs
#[derive(Debug, Args)]
pub struct QueryCommand { /* params */ }
#[async_trait::async_trait]
impl RunCommand for QueryCommand {
async fn run(&self) -> anyhow::Result<()> {
// Your command implementation here!
}
}
```
##### `mod.rs`
Finally tying it all together:
```rs
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
command: Option<Commands>,
// Any global options that apply to all commands go below!
#[arg(short, long)]
pub verbosity: Option<String>,
}
#[async_trait::async_trait]
impl RunCommand for Cli {
async fn run(&self) -> anyhow::Result<()> {
if let Some(command) = &self.command {
command.run().await?;
exit(0);
} else {
Ok(())
}
}
}
#[derive(Debug, Subcommand)]
pub enum Commands {
Account(Account),
Query(QueryCommand),
}
#[async_trait::async_trait]
impl RunCommand for Commands {
async fn run(&self) -> anyhow::Result<()> {
match self {
Commands::Account(account) => account.run().await,
Commands::Query(query) => query.run().await,
}
}
}
```
Now, in `main` all you have to do is call your trait:
```rs
let cli = Cli::parse();
cli.run().await.unwrap();
```
I love the filesystem-routing like feel of this. Whenever you see a command in
the CLI, you immediately know what file to go to if you need to change or fix
something. Please don't discount the value of this! As your application grows,
it can be a massive headache to come across a bug and then having to dig around
the entire codebase to find where that code is coming from. If all commands have
a particular file, you can always start your search from that file and use "go
to definition" to dig into the implementation.
This also helps if you are adding a new command, it's immediately obvious where
the code needs to go. Simple but elegant, and a significant productivity boost.

View file

@ -0,0 +1,208 @@
---
title: "What I'm enjoying using for mobile app development"
date: 2024-01-02T04:33:55Z
draft: false
toc: false
images:
tags:
- dev
- react
- mobile
- typescript
---
I like baking bread. Well, most of all I love bread, and baking it myself is
more convenient than tracking down a good bakery nearby. I used to bake a lot
--and as a hipster I'll tell you that it was before everyone else started doing
it at the start of the pandemic-- but I got bored of it some time ago and quit.
But I've been eating more bread recently, and decided I could bake something
better, and cheaper, than the bread from the grocery store. I'm pretty happy
with what I've been baking too, they are all turning out with a good crust and
soft insides. What's the inside of the bread called? Anyway, my bread is good
but a little bland, so I decided to also get back into sourdough bread to get
some nice flavor.
Sourdough bread is made with sourdough starter, a live culture of yeast and
bacteria that needs to be maintained with regular feedings of flour. So before
you can bake sourdough bread, you must first prepare your starter and grow it
for a few weeks to get its strength up, then keep it alive for as long as you
want to keep baking. And before you can prepare a sourdough starter, you must
first develop a mobile app.
Well, of course I could have just set up reminder in my calendar and skip having
to develop an actual app. Or just use on of the proprietary ones. But why do the
easy thing when you can waste tens --if not hundreds-- of hours developing an
app to maybe save a few seconds of your time or to make a point about using open
source software? At least it will teach me some stuff along the way.
## What tech to use?
But before I could start developing an app, I had to decide what to build it
with. My first two requirements: it had to be cross platform, and it had to be
able to show scheduled notifications on my phone. My first thought was to use a
PWA. PWAs are web apps that can be "installed" on your phone like a regular app,
but run entirely on your browser. It's sort of an Electron-lite. That would be
great since I already know a lot about web development, alas PWAs can't send
scheduled notifications while the app is in the background. That's a pretty
vital part of this project so that rules out PWAs. Also I think Safari has some
problems with PWAs? So that also would have ruled it out if I somehow worked
around the notifications.
Similar to PWAs, I could have used something like Electron to develop an app
using web technologies. In this case it would be an actual app, so I could
actually run code in the background and send scheduled notifications. Hurray!
Except that I wasn't sure I wanted to do this. My experience with apps developed
this way is terrible, and it's often very obvious someone basically put a web
browser in kiosk mode. These apps are often slow, the UIs are completely devoid
of the native look and feel, and they are often full of weird browserisms like
being able to hold and select the text on the page. Maybe this perception is a form of [Survivorship bias](https://en.wikipedia.org/wiki/Survivorship_bias#In_the_military)
and there are lots of apps I used developed with these technologies that work great,
but I decided to see what else was available first.
After more digging, I finally narrowed my choices down to 2 options: React
Native and Flutter. React Native takes the usual React, and instead of rendering the virtual DOM into a real DOM it renders it
to native UI components. React Native is something I used before, with my [Bulgur Cloud](https://bulgur-cloud.github.io) project.
While I liked React Native, I actually ended up rewriting Bulgur Cloud --in Next.js-- so I was a little hesitant.
Flutter on the other hand uses Dart, and is developed by Google with a focus on cross platform apps.
Flutter works by rendering everything to a canvas... which sort of surprised me.
My biggest concern with this was performance and accessibility, using native components
gives you both out of the box. If you render everything yourself, then it's
your responsibility to handle accessibility and optimize rendering.
Articles and discussions I came across online reassure me that Flutter is "doing better" at all of these,
so hopefully it's not a problem.
At this point I was still paralyzed by this decision, so I ended up creating a project with both to check out
the out of the box example. Which really did not help, they both work and look fine.
The code is pretty easy to understand with both. I have concerns with the future of both projects,
because React Native feels too under control of Expo and Flutter feels too under control of Google.
I finally broke my paralysis by just going with React Native. I already know
React, I know a ton of stuff about the React ecosystem, I've already used React
Native before. Flutter and the Dart language look fine and I'm sure I could
learn them, but it's just easier for me to get this project off the ground if I
use what I already know. I also remembered that the reason why I rewrote Bulgur
Cloud was because React Native for Web is a bad experience, but in this case I
don't care about the web so it's not a problem.
## To Expo, or not to Expo?
When you start using React Native, the first thing you see is Expo. Look at the
official getting started guide for React Native and it immediately recommends
you use Expo. Expo then keeps coming up again and again in the docs. It's easy
to see why:
- Expo solves a major getting started hurdle: installing Android Studio or XCode
and connecting it to your phone so you can run it. Instead, you install the
Expo Go app from your phones app store and scan a QR code to get started
immediately.
- Expo comes with a lot of built-in packages. If you go with Expo, you can use
all these packages *and* you can still use all the regular React Native
packages.
- Expo doesn't technically lock you into anything, you can "eject" from Expo at
any time.
I decided to use Expo. While I have some concerns that Expo is an
investor-backed company and not a community effort, the Expo packages are all
open source. Expo Go is open source. And you can still install Android Studio
and XCode and build everything yourself.
## What else?
Expo comes with a lot out of the box, but there were some packages I wanted to
play with and some packages I knew I wanted on top of that. Some of these include:
### Tamagui
[Tamagui](https://tamagui.dev) is a UI kit for React Native. I first saw it
advertised as a "headless" library meaning that it implements all the
functionality but comes with no styles, so you style everything yourself. I
later found out that this was not entirely correct though because Tamagui does
come with default styles, but lets you opt out of them or customize them.
I really like Tamagui because it comes as a complete package. It's not just UI
components but also animations, CSS shorthands and a token based design system.
It also comes with a cool "sub-theme" system that allows you to have variants of
your themes, so you don't just have a "dark" theme but you can also have
"dark-forest" and "dark-blue" and "dark-whatever" to create different themes for
different sections of you app without having to hand-code everything.
### Expo-Sqlite
[Expo-sqlite](https://docs.expo.dev/versions/latest/sdk/sqlite/) is an expo package, but it's an optional one.
It gives you Sqlite. That's about all. Sqlite is awesome, so why not.
There are some other options for storage, some even support additional features like encryption
if that's something you need. But I don't think it's crucial if I encrypt
my sourdough starter data, so I'd rather take the Sqlite features.
### nearform/sql
[@nearform/sql](https://github.com/nearform/sql) is a library for SQL injection
prevention. What's cool about it is that you get to write SQL queries with the
javascript string interpolation without risking SQL injection. So you can do:
```js
DB.query(SQL`INSERT INTO students(name) VALUES (${name})`);
```
without little [Bobby Tables](https://xkcd.com/327/) ruining your day.
nearform/sql is not compatible with Expo-sqlite out of the box, but a basic
wrapper function can easily get you there. Here it is, I'd
publish it on npm but it's so short you might as well just copy and paste it.
```ts
export function sql(strings: any, ...values: any[]): Query {
const statement = SQL(strings, ...values);
return {
sql: statement.text,
args: statement.values,
};
}
```
### SWR
[SWR](https://swr.vercel.app) describes itself as "React Hooks for Data
Fetching". My little app is local only and doesn't need to fetch data from
anywhere, so why SWR? Well, SWR is great whenever you need to get data from
anywhere outside of your apps own state. In my case, I need to get data out of
SQLite and into my app, so I use SWR to fetch the data from the database.
The main use case for me is the caching and deduplication. SWR caches results
and won't fetch again if the same hook is called multiple times, so you can use
your hooks everywhere without having to worry about the same data being fetched
multiple times for no reason. You can also invalidate the cache whenever you
want, which will invalidate all users of the hook and fetch the data just once
to update everything.
You need some workarounds to get SWR working in React Native, but [official docs have you covered](https://swr.vercel.app/docs/advanced/react-native.en-US).
### Formik
I was hesitant and I dragged my feet the very first time I encountered
[Formik](https://formik.org), but after trying it once I actually fell in love
with it. It really does everything you need a form to do. The best part to me is
that you get to avoid having a million `useState`s in your form, and thanks to
the Formik context you can organize your code without drilling values and
setters down into components.
You have to do a bit of work for Formik to work in React Native, but the [official guide tells you what to do](https://formik.org/docs/guides/react-native)
so it's an easy change. Wrap that in a custom form input component and you can forget
the incompatibility exists at all.
## Some More Packages
This is getting really long (for my usual posts), so a few more quick mentions:
- `date-fns` is the definitive date & time library. It's incredible.
- `radash` is like lodash. I'm honestly not fully sure if it's better or worse, but I do keep coming back to it.
- `rrule` makes calculating recurring dates easy. You can serialize and deserialize your rrule's which I also appreciate.
- `ulidx` is a random ID generator. It's like nanoid, but at the cost of just a few more characters they are ordered by creation date.
- `zod` is my favorite schema tool. Never leave home without it.
## Fin
I'm about done with the basic functionality I wanted for this app, so I'm hoping
to have a version of this up on the Google Play store by the end of the week.
Oh and I still haven't made that sourdough starter,

Some files were not shown because too many files have changed in this diff Show more