Compare commits
20 Commits
8657958135
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bafec4821 | ||
|
|
d36c6d5622 | ||
|
|
e4d6a9df56 | ||
|
|
77f9310786 | ||
|
|
967a9a3ebd | ||
|
|
0c3ac11c61 | ||
|
|
fe519c46e7 | ||
|
|
01db7a8bb1 | ||
|
|
15aa4575bc | ||
|
|
6f687d22c2 | ||
|
|
ad775e4f77 | ||
|
|
c4c9959516 | ||
| 8f63b56619 | |||
| 16bc996103 | |||
| 6174a406ec | |||
| 75e08d3493 | |||
| 4a481defb9 | |||
| 50201ec1fe | |||
| 99dba7d0c3 | |||
|
|
6ba363c2c9 |
42
.gitea/workflows/deploy.yml
Normal file
42
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Upload via SSH
|
||||
|
||||
on:
|
||||
# Trigger the workflow every time you push to the `main` branch
|
||||
# Using a different branch name? Replace `main` with your branch’s name
|
||||
push:
|
||||
branches: [ main ]
|
||||
# Allows you to run this workflow manually from the Actions tab on GitHub.
|
||||
workflow_dispatch:
|
||||
|
||||
# Allow this job to clone the repo and create a page deployment
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install node
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install nodejs -y
|
||||
- name: Checkout your repository using git
|
||||
uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
- name: Sync collection types
|
||||
run: npx astro sync
|
||||
- name: Build your site
|
||||
run: npm run build
|
||||
- name: Deploy
|
||||
uses: https://code.klank.school/riviera/actions-file-deployer@v4
|
||||
with:
|
||||
remote-protocol: "sftp"
|
||||
remote-host: "lbfalvy.com"
|
||||
remote-user: "www-upload"
|
||||
ssh-private-key: ${{ secrets.SSH_KEYFILE }}
|
||||
local-path: "./dist"
|
||||
remote-path: "/lbfalvy.com"
|
||||
sync: full
|
||||
39
.github/workflows/deploy.yml
vendored
39
.github/workflows/deploy.yml
vendored
@@ -1,39 +0,0 @@
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
# Trigger the workflow every time you push to the `main` branch
|
||||
# Using a different branch name? Replace `main` with your branch’s name
|
||||
push:
|
||||
branches: [ main ]
|
||||
# Allows you to run this workflow manually from the Actions tab on GitHub.
|
||||
workflow_dispatch:
|
||||
|
||||
# Allow this job to clone the repo and create a page deployment
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout your repository using git
|
||||
uses: actions/checkout@v4
|
||||
- name: Install, build, and upload your site
|
||||
uses: withastro/action@v3
|
||||
# with:
|
||||
# path: . # The root location of your Astro project inside the repository. (optional)
|
||||
# node-version: 20 # The specific version of Node that should be used to build your site. Defaults to 20. (optional)
|
||||
# package-manager: pnpm@latest # The Node package manager that should be used to install dependencies and build your site. Automatically detected based on your lockfile. (optional)
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
3659
package-lock.json
generated
3659
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -9,20 +9,20 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.0.7",
|
||||
"@astrojs/react": "^4.1.6",
|
||||
"@astrojs/rss": "^4.0.11",
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"@astrojs/svelte": "^7.0.4",
|
||||
"@js-temporal/polyfill": "^0.4.4",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"astro": "^5.1.9",
|
||||
"@astrojs/mdx": "^4.3.13",
|
||||
"@astrojs/react": "^4.4.2",
|
||||
"@astrojs/rss": "^4.0.14",
|
||||
"@astrojs/sitemap": "^3.6.0",
|
||||
"@astrojs/svelte": "^7.2.4",
|
||||
"@js-temporal/polyfill": "^0.5.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"astro": "^5.16.6",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-toc": "^9.0.0",
|
||||
"shiki": "^2.1.0",
|
||||
"svelte": "^5.19.3",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
"shiki": "^3.20.0",
|
||||
"svelte": "^5.46.1",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,35 +9,47 @@
|
||||
posts: CollectionEntry<"blog">[];
|
||||
}
|
||||
let { posts }: Props = $props();
|
||||
|
||||
let allTags = $derived([...new Set(posts.flatMap(item => item.data.tags))]);
|
||||
|
||||
let allTags = $derived([...new Set(posts.flatMap((item) => item.data.tags))]);
|
||||
|
||||
let selectedTags = $state<SvelteMap<string, boolean>>();
|
||||
|
||||
onMount(() => {
|
||||
const url = new URL(window.location.href);
|
||||
const tagsQry = url.searchParams.get('tags');
|
||||
const defaultTags = tagsQry ? new Set(decodeURIComponent(tagsQry).split('+')) : new Set();
|
||||
selectedTags = new SvelteMap(allTags.map(tag => [tag, defaultTags.has(tag)]));
|
||||
})
|
||||
const tagsQry = url.searchParams.get("tags");
|
||||
const defaultTags = tagsQry
|
||||
? new Set(decodeURIComponent(tagsQry).split("+"))
|
||||
: new Set();
|
||||
selectedTags = new SvelteMap(
|
||||
allTags.map((tag) => [tag, defaultTags.has(tag)]),
|
||||
);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!selectedTags) return;
|
||||
const newTagsQry = encodeURIComponent(
|
||||
[...selectedTags.entries()].filter(([_, v]) => v).map(([k, _]) => k).join('+')
|
||||
[...selectedTags.entries()]
|
||||
.filter(([_, v]) => v)
|
||||
.map(([k, _]) => k)
|
||||
.join("+"),
|
||||
);
|
||||
const url = new URL(window.location.href);
|
||||
if (newTagsQry == url.searchParams.get("tags")) return;
|
||||
if (newTagsQry == "") url.searchParams.delete("tags");
|
||||
else url.searchParams.set("tags", newTagsQry);
|
||||
window.history.pushState(null, "", url.toString());
|
||||
})
|
||||
});
|
||||
|
||||
let isFiltered = $derived(selectedTags && [...selectedTags.values()].includes(true));
|
||||
let filteredPosts = $derived(isFiltered
|
||||
? posts.filter(p => !p.data.unlisted && p.data.tags.some(t => selectedTags!.get(t)))
|
||||
: posts.filter(p => !p.data.unlisted));
|
||||
let shownPosts = $derived(filteredPosts.toReversed())
|
||||
let isFiltered = $derived(
|
||||
selectedTags && [...selectedTags.values()].includes(true),
|
||||
);
|
||||
let listedPosts = $derived(posts.filter((p) => !p.data.unlisted));
|
||||
let filteredPosts = $derived(
|
||||
isFiltered
|
||||
? listedPosts.filter((p) => p.data.tags.some((t) => selectedTags!.get(t)))
|
||||
: listedPosts,
|
||||
);
|
||||
let shownPosts = $derived([...filteredPosts].reverse());
|
||||
</script>
|
||||
|
||||
<div class="lg:flex flex-row-reverse justify-between items-start">
|
||||
@@ -46,31 +58,37 @@
|
||||
{#each allTags as tag}
|
||||
<button
|
||||
onclick={() => {
|
||||
console.log("TRoggled tag")
|
||||
selectedTags!.set(tag, !selectedTags!.get(tag))
|
||||
console.log("TRoggled tag");
|
||||
selectedTags!.set(tag, !selectedTags!.get(tag));
|
||||
}}
|
||||
class={[
|
||||
"m-0.5 rounded-4xl emph-bg px-2 cursor-pointer border-2 border-solid",
|
||||
selectedTags?.get(tag) && "border-gray-400"
|
||||
selectedTags?.get(tag) && "border-gray-400",
|
||||
]}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<main class="max-w-[650px]">
|
||||
<main class="max-w-650px">
|
||||
{#each shownPosts as post}
|
||||
<article>
|
||||
<a href={`/blog/${post.id}`}>
|
||||
<div class="
|
||||
<div
|
||||
class="
|
||||
lg:grid grid-cols-[auto_min-content_min-content]
|
||||
m-1 p-2 hover:bg-shade
|
||||
">
|
||||
"
|
||||
>
|
||||
<h2 class="font-bold">{post.data.title}</h2>
|
||||
<address class="inline-block post-meta col-start-2 md:ml-3">
|
||||
<address
|
||||
class="inline-block post-meta col-start-2 md:ml-3 whitespace-nowrap"
|
||||
>
|
||||
{post.data.author}
|
||||
</address>
|
||||
<div class="inline-block post-meta col-start-3 lg:ml-1">
|
||||
<div
|
||||
class="inline-block post-meta col-start-3 lg:ml-1 whitespace-nowrap"
|
||||
>
|
||||
<Time datetime={post.data.pubDate} />
|
||||
</div>
|
||||
<div class="col-span-3">{post.data.summary}</div>
|
||||
@@ -79,4 +97,4 @@
|
||||
</article>
|
||||
{/each}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
103
src/components/Fortune.tsx
Normal file
103
src/components/Fortune.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React from "react"
|
||||
|
||||
const messages = [
|
||||
"Test message, please ignore",
|
||||
|
||||
"Often when people ask difficult questions they don't actually need the answer but rather \
|
||||
fail to fully understand their situation, take one too many steps in imitating \
|
||||
familiar solutions, and end up asking the wrong question. This is known as the X/Y problem. \
|
||||
\n\n\
|
||||
On the other hand, just blurting \"X/Y!\" in response to any difficult question is \
|
||||
less useful than direct answers to the bad question. In remembering my learning journey, \
|
||||
when I'm posed a difficult technical question I like to ask back:\
|
||||
\n\n\
|
||||
> This is a difficult problem with no perfect solution, and it's often easy to avoid it altogether.\n\
|
||||
> Is there any more context to this?",
|
||||
|
||||
"This airplane is the choice of professional pilots! \
|
||||
The first thing we advise you to do is turn the yoke upside down, it's much more ergonomic like that. \
|
||||
Also, you probably want to rip out the altimeter and replace it with a custom one that has more features. \
|
||||
And bring your own GPS unit and tape it to the windshield, because this plane doesn't have one by default. \
|
||||
And of course, remember that unlike in your car, 'left' actually means right in this plane. \
|
||||
This is the professional way. Ah yes, when you fly over Canada, \
|
||||
you will see a blinking warning sign telling you that you're out of fuel. \
|
||||
That's just a small glitch caused by metric/imperial confusion. \
|
||||
It has been known for 23 years but we haven't quite gotten around to fixing it yet. \
|
||||
Don't worry, this plane is still excellent for professional pilots. \
|
||||
If you feel otherwise, you are probably not a real professional.",
|
||||
|
||||
"Create React App doesn't support including source files from outside the src folder, or following symlinks.",
|
||||
|
||||
"Seven million years have passed since the first machines were erected by man. \
|
||||
This once-holy land, now cursed with laze and oil, metals, \
|
||||
hoped to fulfill menial tasks of increasingly insignificant bother. When bother became microcosmic, \
|
||||
we tackled problems past the point of physical inconvenience: First, our sadness was tackled. \
|
||||
Feelings injected into our daily routing, tricked the senses with light and sound. \
|
||||
Fear was tackled next - information on everything, transparency to nothing, \
|
||||
a false promise built on a whim by those with real power, power to produce comforting lies. \
|
||||
Then our soul, ideals sold on providence and grandeur in a post-religion world, \
|
||||
people found faith in new gods residing within the computer, scriptures in everyone's pocket, \
|
||||
words of this holy book code, powerful enough to shift the seas without anyone noticing the gentle pull \
|
||||
of its immeasurable strength. \
|
||||
\n\n\
|
||||
There once was a way for all the world to unify into one space, \
|
||||
this coalescence led to the destruction of everything. \
|
||||
\n\n\
|
||||
@pukicho",
|
||||
|
||||
"The quickest way to oust yourself as a foreigner in England is to mention a place.",
|
||||
|
||||
"My informant at https://martinteoharov.com/ has an important message for you",
|
||||
]
|
||||
|
||||
function break_lines(input: string, width: number): string {
|
||||
return input
|
||||
.split("\n")
|
||||
.flatMap(l => {
|
||||
let ret: string[] = [];
|
||||
while (width < l.length) {
|
||||
let split_point = l.lastIndexOf(" ", width);
|
||||
if (split_point < 0) {
|
||||
ret.push(l.slice(0, width));
|
||||
l = l.slice(width);
|
||||
} else {
|
||||
ret.push(l.slice(0, split_point));
|
||||
l = l.slice(split_point + 1);
|
||||
}
|
||||
}
|
||||
ret.push(l);
|
||||
return ret;
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
export default function FortuneComponent(): React.ReactElement {
|
||||
const id = Math.floor(Math.random() * messages.length);
|
||||
window.localStorage.setItem("martinRefShown", id === 6 ? "yes" : "no");
|
||||
const text = break_lines(messages[id], 60);
|
||||
const width = text.split("\n").map(s => s.length).reduce((acc, l) => Math.max(acc, l))
|
||||
return <>
|
||||
<pre>
|
||||
{" " + "_".repeat(width + 2) + "\n"}
|
||||
{text.split("\n").map((l, i, a) => {
|
||||
let [s, e] = a.length === 1 ? ["(", ")"]
|
||||
: i === 0 ? ["/", "\\"]
|
||||
: i + 1 === a.length ? ["\\", "/"]
|
||||
: ["|", "|"]
|
||||
return s + " " + l.padEnd(width) + " " + e + "\n"
|
||||
})}
|
||||
{" " + "-".repeat(width + 2)}
|
||||
{`
|
||||
l
|
||||
l
|
||||
.--.
|
||||
|o_o |
|
||||
|:_/ |
|
||||
// l l
|
||||
(| | )
|
||||
/bl_ _/bl
|
||||
l___)=(___/
|
||||
`.replaceAll("b", "`").replaceAll("l", "\\")}
|
||||
</pre>
|
||||
</>
|
||||
}
|
||||
@@ -4,14 +4,11 @@ import { defineCollection, z } from 'astro:content';
|
||||
import { isValidTime } from './utils/time';
|
||||
|
||||
const blog = defineCollection({
|
||||
// Load Markdown and MDX files in the `src/content/blog/` directory.
|
||||
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
|
||||
// Type-check frontmatter using a schema
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
summary: z.string(),
|
||||
image: z.string().optional(),
|
||||
// Transform string to ZonedDateTime object
|
||||
pubDate: z.string().refine(isValidTime),
|
||||
updatedDate: z.string().refine(isValidTime).optional(),
|
||||
unlisted: z.boolean().optional(),
|
||||
@@ -20,4 +17,13 @@ const blog = defineCollection({
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { blog };
|
||||
const projects = defineCollection({
|
||||
loader: glob({ base: './src/content/projects', pattern: '**/*.{md,mdx}' }),
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
image: z.string().optional(),
|
||||
})
|
||||
})
|
||||
|
||||
export const collections = { blog, projects };
|
||||
|
||||
@@ -4,7 +4,7 @@ author: lbfalvy
|
||||
tags: [programming, rust, langdev]
|
||||
pubDate: 2025-01-29T11:27Z[UTC]
|
||||
summary: On the state of async Rust, limitations of the type system, and Iterator::map
|
||||
image: https://assets-for-gh-pages.gb-lon-1.linodeobjects.com/Screenshot%202025-01-29%20170323.png
|
||||
image: https://eu2.contabostorage.com/b5fca1793241407290cf2335ba076411:assets/lbfalvy_com/Screenshot 2025-01-29 170323.png
|
||||
unlisted: false
|
||||
---
|
||||
import Graphic from "../../components/Graphic.astro"
|
||||
@@ -20,7 +20,7 @@ In synchronous rust, `Iterator::map` takes an `FnMut`, a function which can only
|
||||
fn map<B, F>(self, f: F) -> Map<Self, F> where F: FnMut(Self::Item) -> B;
|
||||
}
|
||||
```
|
||||
<p slot="label">source (modified): https://doc.rust-lang.org/src/core/iter/traits/iterator.rs.html#745</p>
|
||||
<p slot="label">modified from the standard library</p>
|
||||
</Graphic>
|
||||
|
||||
The async equivalent however has to take a function that returns some type that implements `Future` because that's how you statically type an asynchronous function, you parameterize on the state machine the compiler will eventually generate for its paused data. This is again perfectly normal, C++ coroutines do the same as I'm pretty sure every other language that supports any kind of stack-allocated coroutine has to. The problem emerges from lifetimes, because in order for that Future to hold onto a mutable reference, the async equivalent of map (which happens to be called `StreamExt::then` for reference) has to not only guarantee that the callback will not be running when its next called, but that its return value (the `Future` instance) will not exist (either because it's finished or because it's been freed) by the time the function is called again.
|
||||
@@ -41,7 +41,7 @@ The async equivalent however has to take a function that returns some type that
|
||||
<p slot="label">source (modified): https://docs.rs/futures-util/0.3.31/src/futures_util/stream/stream/mod.rs.html#488</p>
|
||||
</Graphic>
|
||||
|
||||
The type of the callback then _should be_ **some function which for any lifetime `'a` returns some type is valid for the same lifetime `'a`**. The type of the return value is parametric!
|
||||
The type of the callback then _should be_ **some function which for any lifetime `'a` returns some type that is valid for the same lifetime `'a`**. The type of the return value is parametric!
|
||||
|
||||
Since structs and functions can only be parametric on concrete types, not generics, a callback whose return type has a different contract depending on how you called the function is illegal on general. So if you want to access mutable data in an async stream, you have to make an ad-hoc `Mutex<&mut T>` right there on the stack which the closure and its return value can capture by shared reference and then immediately lock for its entire runtime. Streams are lazy and a new value will not be pulled until the current one is finished so this mutex can never ever be contested, but there is no way at all to explain this to the type system.
|
||||
|
||||
|
||||
6
src/content/projects/00-orchid.mdx
Normal file
6
src/content/projects/00-orchid.mdx
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: Orchid
|
||||
url: https://github.com/lbfalvy/orchid
|
||||
image: https://raw.githubusercontent.com/lbfalvy/orchid/refs/heads/master/icon.svg
|
||||
---
|
||||
An embeddable functional scripting language I've been developing for many years now both as a recreational activity and to learn Rust.
|
||||
6
src/content/projects/10-f.engine.mdx
Normal file
6
src/content/projects/10-f.engine.mdx
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: f.engine
|
||||
url: https://lbfalvy.github.io/f.engine/
|
||||
image: https://eu2.contabostorage.com/b5fca1793241407290cf2335ba076411:assets/lbfalvy_com/f-engine.png
|
||||
---
|
||||
A small lambda calculus executor made in Javascript to demonstrate lambda calc to my friends. It works with repeated string splicing so it gets very slow.
|
||||
5
src/content/projects/20-mockable-timer.mdx
Normal file
5
src/content/projects/20-mockable-timer.mdx
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
name: Mockable Timer
|
||||
url: https://github.com/lbfalvy/mockable-timer
|
||||
---
|
||||
An API that replaces `Date.now()`, `setTimeout` and `setImmediate` for dependency injection, and a carefully crafted mock implementation with an easy to use interface.
|
||||
5
src/content/projects/30-react-context-menu.mdx
Normal file
5
src/content/projects/30-react-context-menu.mdx
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
name: React Context Menu
|
||||
url: https://www.npmjs.com/package/@lbfalvy/react-context-menu
|
||||
---
|
||||
A multilevel custom context menu for React. The API relies on React context so that menus can be combined within nested containers
|
||||
5
src/content/projects/40-simple-rtr.mdx
Normal file
5
src/content/projects/40-simple-rtr.mdx
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
name: Simple RTR
|
||||
url: https://github.com/lbfalvy/buffered-dispatch
|
||||
---
|
||||
A library for Refresh Token Rotation that can scale to many concurrent open tabs without a service worker.
|
||||
4
src/icons/Bluesky_Logo.svg
Normal file
4
src/icons/Bluesky_Logo.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="600" height="530" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z" fill="#1185fe"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 745 B |
1
src/icons/Gitea_Logo.svg
Normal file
1
src/icons/Gitea_Logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" style="enable-background:new 0 0 640 640;" xml:space="preserve" viewBox="5.67 143.05 628.65 387.55"> <g> <path id="teabag" style="fill:#FFFFFF" d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8 c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4 c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"/> <g> <g> <path style="fill:#609926" d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2 c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5 c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5 c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3 c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1 C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4 c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7 S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55 c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8 l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"/> <path style="fill:#609926" d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4 c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1 c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9 c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3 c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3 c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29 c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8 C343.2,346.5,335,363.3,326.8,380.1z"/> </g> </g> </g> </svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
11
src/icons/PATREON_WORDMARK_1_WHITE_RGB.svg
Normal file
11
src/icons/PATREON_WORDMARK_1_WHITE_RGB.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 94.33">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-1" d="M11.5,13.79c0-3.35,2.49-5.62,6.38-5.62h18.59c16.86,0,28.32,9.29,28.32,22.91s-11.35,23.34-28.32,23.34h-2.59c-6.49,0-9.84,3.35-9.84,8.75v15.24c0,4.32-2.49,7.02-6.27,7.02s-6.27-2.7-6.27-7.02V13.79ZM24.04,33.14c0,6.81,3.46,10.05,10.05,10.05h1.62c9.3,0,16.1-3.78,16.1-12.1s-6.81-12.1-16.1-12.1h-1.62c-6.59,0-10.05,3.24-10.05,10.05v4.11ZM64.79,79.5c0,3.57,2.49,5.94,6.27,5.94,2.49,0,4.76-1.62,6.05-4.97l2.27-6.05c1.84-4.86,5.08-7.13,8.65-7.13h20.54c3.57,0,6.81,2.27,8.65,7.13l2.27,6.05c1.3,3.35,3.57,4.97,6.05,4.97,3.78,0,6.27-2.38,6.27-5.94,0-.97-.22-2.16-.65-3.35l-24.54-63.76c-1.51-3.89-4.97-5.84-8.32-5.84s-6.81,1.95-8.32,5.84l-24.54,63.76c-.43,1.19-.65,2.38-.65,3.35ZM88.67,50.54c0-1.19.32-2.27.86-3.89l4.65-12.86c.86-2.49,2.38-3.67,4.11-3.67s3.24,1.19,4.11,3.67l4.65,12.86c.54,1.62.86,2.7.86,3.89,0,3.24-1.84,5.51-6.7,5.51h-5.84c-4.86,0-6.7-2.27-6.7-5.51ZM127.69,14.01c0-3.46,2.49-5.84,6.49-5.84h49.72c4,0,6.49,2.38,6.49,5.84s-2.49,5.84-6.49,5.84h-8.32c-6.59,0-10.16,3.35-10.16,11.02v47.44c0,4.43-2.49,7.13-6.38,7.13s-6.38-2.7-6.38-7.13V30.87c0-7.67-3.57-11.02-10.16-11.02h-8.32c-4,0-6.49-2.38-6.49-5.84ZM202.06,78.42c0,4.32,2.49,7.02,6.27,7.02s6.27-2.7,6.27-7.02v-17.18c0-4.86,2.81-6.92,6.27-6.92h.87c2.27,0,4.54,1.4,5.94,3.46l16.43,24.1c1.51,2.27,3.46,3.57,5.94,3.57,3.24,0,5.84-2.7,5.84-5.94,0-1.3-.43-2.7-1.4-4.11l-10.92-15.35c-1.3-1.84-1.84-3.35-1.84-4.65,0-2.7,2.38-4.65,5.19-6.7,5.08-3.78,10.59-8.75,10.59-18.26,0-13.29-10.38-22.26-27.45-22.26h-21.72c-3.89,0-6.27,2.27-6.27,5.62v64.63ZM214.59,32.17v-3.24c0-7.02,3.68-9.94,9.3-9.94h5.4c9.29,0,15.24,3.46,15.24,11.46s-6.27,11.67-15.56,11.67h-5.08c-5.62,0-9.3-2.92-9.3-9.94ZM273.5,78.21V13.79c0-3.35,2.38-5.62,6.27-5.62h40.86c3.89,0,6.27,2.27,6.27,5.62s-2.38,5.62-6.27,5.62h-25.83c-5.08,0-8.76,3.03-8.76,8.75v2.38c0,5.73,3.68,8.75,8.76,8.75h20c3.89,0,6.27,2.27,6.27,5.62s-2.38,5.62-6.27,5.62h-19.24c-5.08,0-9.51,3.13-9.51,9.51v3.03c0,6.38,4.43,9.51,9.51,9.51h25.08c3.89,0,6.27,2.27,6.27,5.62s-2.38,5.62-6.27,5.62h-40.86c-3.89,0-6.27-2.27-6.27-5.62ZM334.35,46c0-23.34,17.51-39.45,38.05-39.45s38.05,16.1,38.05,39.45-17.51,39.45-38.05,39.45-38.05-16.1-38.05-39.45ZM348.62,46c0,16.43,9.73,26.91,23.78,26.91s23.78-10.48,23.78-26.91-9.73-26.91-23.78-26.91-23.78,10.37-23.78,26.91ZM425.47,78.42c0,4.32,2.49,7.02,6.27,7.02s6.27-2.7,6.27-7.02v-33.07c0-4,2.38-5.94,4.86-5.94,1.95,0,3.57,1.08,4.76,3.03l20.75,34.69c2.81,4.76,5.4,8.32,10.7,8.32s8.76-3.67,8.76-9.62V13.58c0-4.32-2.49-7.02-6.27-7.02s-6.27,2.7-6.27,7.02v33.07c0,4-2.38,5.94-4.86,5.94-1.94,0-3.57-1.08-4.76-3.03l-20.75-34.69c-2.81-4.76-5.4-8.32-10.7-8.32s-8.76,3.67-8.76,9.62v62.25Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
|
||||
|
Before Width: | Height: | Size: 960 B |
5
src/icons/matrix.svg
Normal file
5
src/icons/matrix.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg fill="#FFFFFF" xmlns="http://www.w3.org/2000/svg" viewBox="4 7 42 36" width="50px"
|
||||
height="50px">
|
||||
<path
|
||||
d="M 5 5 A 1.0001 1.0001 0 0 0 4 6 L 4 44 A 1.0001 1.0001 0 0 0 5 45 L 8 45 A 1.0001 1.0001 0 1 0 8 43 L 6 43 L 6 7 L 8 7 A 1.0001 1.0001 0 1 0 8 5 L 5 5 z M 42 5 A 1.0001 1.0001 0 1 0 42 7 L 44 7 L 44 43 L 42 43 A 1.0001 1.0001 0 1 0 42 45 L 45 45 A 1.0001 1.0001 0 0 0 46 44 L 46 6 A 1.0001 1.0001 0 0 0 45 5 L 42 5 z M 31.074219 17.509766 C 29.975744 17.487506 28.868391 17.760297 27.978516 18.373047 C 27.407516 18.767047 26.915609 19.272813 26.349609 19.757812 C 25.488609 18.039813 23.929344 17.580781 22.152344 17.550781 C 20.351344 17.519781 18.920922 18.341797 17.669922 19.841797 L 17.669922 18 L 14 18 L 14 32 L 17.664062 32 C 17.664062 32 17.657969 26.766016 17.667969 24.166016 C 17.669969 23.704016 17.689203 23.23225 17.783203 22.78125 C 18.073203 21.39225 19.538031 20.534437 20.957031 20.648438 C 22.309031 20.757437 23.100016 21.495656 23.166016 23.097656 C 23.177016 23.376656 23.166016 32 23.166016 32 L 26.832031 32 L 26.832031 24.228516 C 26.838031 23.629516 26.901875 23.0175 27.046875 22.4375 C 27.372875 21.1375 28.590531 20.49825 30.019531 20.65625 C 31.279531 20.79525 32.239031 21.474609 32.332031 22.849609 L 32.332031 32 L 36 32 L 36 22 C 36 21 35.746359 20.490859 35.443359 19.880859 C 34.710859 18.405234 32.90501 17.546865 31.074219 17.509766 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,24 +1,60 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import Main from './Main.astro';
|
||||
import Time from '../components/Time.svelte';
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import Main from "./Main.astro";
|
||||
import Time from "../components/Time.svelte";
|
||||
import { Image } from "astro:assets";
|
||||
import patreonWordmark from "../icons/PATREON_WORDMARK_1_WHITE_RGB.svg";
|
||||
|
||||
type Props = CollectionEntry<'blog'>['data'];
|
||||
type Props = CollectionEntry<"blog">["data"];
|
||||
|
||||
const { title, author, summary, pubDate, updatedDate, image } = Astro.props;
|
||||
---
|
||||
|
||||
<Main title={title} description={summary} image={image}>
|
||||
<article class="m-5 mt-3">
|
||||
<header class="lg:grid grid-cols-[auto_auto_minmax(300px,_1fr)] grid-rows-[auto_auto]">
|
||||
<header
|
||||
class="lg:grid grid-cols-[auto_auto_minmax(300px,1fr)] grid-rows-[auto_auto]"
|
||||
>
|
||||
<h2 class="font-bold row-span-2 text-2xl m-2 mt-3">{title}</h2>
|
||||
<address class="post-meta inline-block col-start-2">{author}</address>
|
||||
<Time client:load datetime={pubDate} />
|
||||
<div class="italic tracking-[3px] text-emph-fg col-start-2 col-span-2 m-2 mt-0">{summary}</div>
|
||||
{updatedDate && <div>Amended <Time datetime={updatedDate} /></div>}
|
||||
<div
|
||||
class="italic tracking-[3px] text-emph-fg col-start-2 col-span-2 m-2 mt-0"
|
||||
>
|
||||
{summary}
|
||||
</div>
|
||||
{
|
||||
updatedDate && (
|
||||
<div>
|
||||
Amended <Time datetime={updatedDate} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</header>
|
||||
<hr class="mb-3">
|
||||
<hr class="mb-3" />
|
||||
<main class="max-w-[100ch] font-prose post-content">
|
||||
<slot />
|
||||
</main>
|
||||
<footer class="mt-20">
|
||||
<p class="post-meta">
|
||||
I pledge to spend all the extra free time on silly experiments.
|
||||
</p>
|
||||
<a href="https://ko-fi.com/D1D4AFWZX" target="_blank">
|
||||
<img
|
||||
class="inline-block border-0 h-6"
|
||||
src="https://storage.ko-fi.com/cdn/kofi1.png?v=6"
|
||||
alt="Buy Me a Coffee at ko-fi.com"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://patreon.com/lbfalvy?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink"
|
||||
>
|
||||
<Image
|
||||
class="inline-block border-0 h-4 w-22.5 object-cover ml-3"
|
||||
src={patreonWordmark}
|
||||
alt="Support me on Patreon"
|
||||
/>
|
||||
</a>
|
||||
</footer>
|
||||
</article>
|
||||
</Main>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
---
|
||||
import { Image } from "astro:assets";
|
||||
import NavLink from "../components/NavLink.astro";
|
||||
import Layout from "../layouts/Html.astro"
|
||||
import Layout from "../layouts/Html.astro";
|
||||
import "../styles/global.css";
|
||||
import GhLogo from "../icons/github-mark-white.svg";
|
||||
import GiteaLogo from "../icons/Gitea_Logo.svg";
|
||||
import RssLogo from "../icons/rss.svg";
|
||||
import BskyLogo from "../icons/Bluesky_Logo.svg";
|
||||
import MatrixLogo from "../icons/matrix.svg";
|
||||
import { SITE_DESCRIPTION, SITE_TITLE } from "../consts";
|
||||
|
||||
interface Props {
|
||||
@@ -18,40 +20,84 @@ const {
|
||||
description = SITE_DESCRIPTION,
|
||||
image,
|
||||
} = Astro.props;
|
||||
|
||||
---
|
||||
|
||||
<Layout title={title} description={description} image={image}>
|
||||
<Fragment slot="head">
|
||||
<meta name="theme-color" content="#222" />
|
||||
<link rel="icon" type="image/png" href="https://github.com/lbfalvy.png"/>
|
||||
<link rel="icon" type="image/png" href="https://github.com/lbfalvy.png" />
|
||||
</Fragment>
|
||||
<div class="
|
||||
w-screen h-screen
|
||||
<div
|
||||
class="w-screen h-screen
|
||||
flex flex-col
|
||||
md:grid
|
||||
md:grid-rows-[1fr_auto] md:grid-cols-[min-content_auto]
|
||||
">
|
||||
<header class="
|
||||
emph-bg whitespace-nowrap md:px-3 py-2 flex flex-col
|
||||
text-right
|
||||
">
|
||||
<h1 class="font-bold text-2xl m-2 mr-0 text-wrap lg:text-nowrap">Lawrence Bet…</h1>
|
||||
<nav class="gutter
|
||||
md:grid-rows-[1fr_auto] md:grid-cols-[min-content_auto]"
|
||||
>
|
||||
<header
|
||||
class="emph-bg whitespace-nowrap md:px-3 py-2 flex flex-col
|
||||
text-right"
|
||||
>
|
||||
<h1 class="font-bold text-2xl m-2 mr-0 text-wrap lg:text-nowrap">
|
||||
Lawrence Bet…
|
||||
</h1>
|
||||
<nav
|
||||
class="gutter
|
||||
flex md:flex-col
|
||||
text-center md:text-right">
|
||||
<NavLink href="/" zone="/blog">Blog</NavLink>
|
||||
<NavLink href="/projects">Projects</NavLink>
|
||||
<NavLink href="/about">About me</NavLink>
|
||||
<NavLink href="/fortune">Fortune</NavLink>
|
||||
</nav>
|
||||
</header>
|
||||
<main id="scroll-area" class="md:row-start-1 md:col-start-2 md:row-span-2 md:overflow-y-auto">
|
||||
text-center md:text-right"
|
||||
>
|
||||
<NavLink href="/" zone="/blog">Blog</NavLink>
|
||||
<NavLink href="/projects">Projects</NavLink>
|
||||
<NavLink href="/about">About me</NavLink>
|
||||
<NavLink href="/fortune">Fortune</NavLink>
|
||||
</nav>
|
||||
</header>
|
||||
<main
|
||||
id="scroll-area"
|
||||
class="md:row-start-1 md:col-start-2 md:row-span-2 md:overflow-y-auto"
|
||||
>
|
||||
<slot />
|
||||
</main>
|
||||
<footer class="md:row-start-2 emph-bg flex justify-center gap-3 py-3">
|
||||
<a href="https://github.com/lbfalvy"><Image height="20" width="20" src={GhLogo} alt="Github"/></a>
|
||||
<a href="/rss.xml"><Image height="20" width="20" src={RssLogo} alt="Rss"/></a>
|
||||
<a href="https://git.lbfalvy.com">
|
||||
<Image
|
||||
height="20"
|
||||
width="34"
|
||||
src={GiteaLogo}
|
||||
alt="Gitea"
|
||||
loading="eager"
|
||||
/>
|
||||
</a>
|
||||
<a href="/rss.xml">
|
||||
<Image height="20" width="20" src={RssLogo} alt="Rss" loading="eager" />
|
||||
</a>
|
||||
<a href="https://bsky.app/profile/lbfalvy.com">
|
||||
<Image
|
||||
height="20"
|
||||
width="25"
|
||||
src={BskyLogo}
|
||||
alt="BlueSky"
|
||||
loading="eager"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://fosstodon.org/@lbfalvy">
|
||||
<img
|
||||
src="https://cdn.fosstodon.org/site_uploads/files/000/000/007/48/91cda647d1c18689.png"
|
||||
alt="Fosstodon"
|
||||
height="20"
|
||||
width="20"
|
||||
loading="eager"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://matrix.to/#/@lbfalvy:matrix.org">
|
||||
<Image
|
||||
height="20"
|
||||
width="22"
|
||||
color="white"
|
||||
src={MatrixLogo}
|
||||
alt="Chat over Matrix"
|
||||
loading="eager"
|
||||
/>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</Layout>
|
||||
@@ -59,9 +105,9 @@ const {
|
||||
<script>
|
||||
const key = `scroll:${window.location.pathname}`;
|
||||
const scrollArea = document.getElementById("scroll-area");
|
||||
scrollArea?.addEventListener('scrollend', () => {
|
||||
sessionStorage.setItem(key, scrollArea.scrollTop.toString())
|
||||
})
|
||||
scrollArea?.addEventListener("scrollend", () => {
|
||||
sessionStorage.setItem(key, scrollArea.scrollTop.toString());
|
||||
});
|
||||
const savedPos = sessionStorage.getItem(key);
|
||||
if (savedPos) scrollArea!.scrollTop = Number.parseFloat(savedPos);
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,40 +1,34 @@
|
||||
---
|
||||
import { Image } from "astro:assets";
|
||||
import Main from "../layouts/Main.astro";
|
||||
|
||||
---
|
||||
|
||||
<Main
|
||||
title="About Me"
|
||||
description="Lawrence Bethlenfalvy, software engineer"
|
||||
>
|
||||
<Main title="About Me" description="Lawrence Bethlenfalvy, software engineer">
|
||||
<div class="max-w-[80ch] m-5">
|
||||
<img src='https://github.com/lbfalvy.png'
|
||||
class="rounded-full h-30 float-right m-3 [shape-outside:_ellipse()]" />
|
||||
<Image
|
||||
src="https://github.com/lbfalvy.png"
|
||||
loading="eager"
|
||||
alt="My face"
|
||||
width="120"
|
||||
height="120"
|
||||
class="rounded-full h-30 float-right m-3 [shape-outside:ellipse()]"
|
||||
/>
|
||||
<p>
|
||||
My name is Lawrence Bethlenfalvy, I make websites and web-based applications primarily
|
||||
with React. I enjoy seeing the fruit of my labour, which is why I do so much
|
||||
frontend development even though I'm not exactly an artistic genius, as finest
|
||||
demonstrated by this website.
|
||||
My name is Lawrence Bethlenfalvy, I make websites and web-based
|
||||
applications primarily with React. I enjoy seeing the fruits of my labour,
|
||||
which is why I do so much frontend development even though I'm not exactly
|
||||
an artistic genius, as finest demonstrated by this website.
|
||||
</p>
|
||||
<p>
|
||||
I really like perfectly designed infrastructure, and in an effort to
|
||||
construct it for my own projects I've racked up a considerable amount of
|
||||
DevOps experience.
|
||||
I like well-oiled infrastructure, and in an effort to construct it for my
|
||||
own projects I've racked up a considerable amount of DevOps experience.
|
||||
</p>
|
||||
<p>
|
||||
I studied a lot of advanced mathematical topics in high school, and although
|
||||
I was never a big fan of solving equations for hours on end the approach and
|
||||
some of the concepts stuck with me. I like to draw on mathematical techniques
|
||||
for day-to-day problem solving, but I also view it as a hobby.
|
||||
</p>
|
||||
<p>
|
||||
More recently I've been hard at work on my small conceptual language, Orchid,
|
||||
which, like anything I do, tries to be unopinionated, minimalistic in design,
|
||||
and robust in execution.
|
||||
Designing a programming language is a constant goat game against the halting problem
|
||||
so realising all these principles at the same time is nigh impossible, and incremental
|
||||
progress is fundamentally incompatible with the optimal, holistic approach to
|
||||
issues like type checking, but when I have something to report I do it here.
|
||||
More recently I've been hard at work on my small conceptual language,
|
||||
Orchid, which, like anything I do, tries to be unopinionated, minimalistic
|
||||
in design, and robust in execution. Incremental progress is fundamentally
|
||||
incompatible with the optimal, holistic approach I like, but when I have
|
||||
something to report I do it here.
|
||||
</p>
|
||||
</div>
|
||||
</Main>
|
||||
|
||||
8
src/pages/fortune.astro
Normal file
8
src/pages/fortune.astro
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import Main from "../layouts/Main.astro";
|
||||
import FortuneComponent from "../components/Fortune";
|
||||
---
|
||||
|
||||
<Main title="Fortune" description="Wisdom from Tux">
|
||||
<FortuneComponent client:only="react" />
|
||||
</Main>
|
||||
37
src/pages/projects.astro
Normal file
37
src/pages/projects.astro
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
import { getCollection, render } from "astro:content";
|
||||
import Main from "../layouts/Main.astro";
|
||||
import { Image } from "astro:assets";
|
||||
|
||||
const projects = await getCollection("projects");
|
||||
const projReady = await Promise.all(
|
||||
projects.map(async (proj) => [proj, (await render(proj)).Content] as const),
|
||||
);
|
||||
---
|
||||
|
||||
<Main>
|
||||
<div class="grid xs:grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 max-w-[100ch]">
|
||||
{
|
||||
projReady.map(([proj, Content]) => (
|
||||
<a href={proj.data.url}>
|
||||
<article class="emph-bg m-3 p-2 text-sm rounded-2xl">
|
||||
{proj.data.image && (
|
||||
<Image
|
||||
src={proj.data.image}
|
||||
alt=""
|
||||
loading="eager"
|
||||
width="100"
|
||||
height="60"
|
||||
class="object-center aspect-video w-full"
|
||||
/>
|
||||
)}
|
||||
<h2 class="font-bold text-xl italic">{proj.data.name}</h2>
|
||||
<div>
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Main>
|
||||
@@ -80,6 +80,7 @@
|
||||
}
|
||||
}
|
||||
@theme {
|
||||
--breakpoint-xs: 25rem;
|
||||
--tw-border-style: solid;
|
||||
--color-emph-bg: #181818;
|
||||
--color-side-bg: #333;
|
||||
@@ -100,7 +101,6 @@
|
||||
color: var(--color-faint-fg);
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
.summary {
|
||||
|
||||
@@ -4,13 +4,18 @@ function lt(one: Temporal.Duration, other: Temporal.DurationLike): boolean {
|
||||
return Temporal.Duration.compare(one, other) < 0
|
||||
}
|
||||
|
||||
export function printTime(time: Temporal.ZonedDateTime): string {
|
||||
const delta = time.until(Temporal.Now.zonedDateTimeISO())
|
||||
export function printTime(datetime: Temporal.ZonedDateTime): string {
|
||||
const delta = datetime.until(Temporal.Now.zonedDateTimeISO())
|
||||
if (lt(delta, { minutes: 1 })) return 'now'
|
||||
if (lt(delta, { minutes: 2 })) return '1 minute ago'
|
||||
if (lt(delta, { hours: 1 })) return `${delta.minutes} minutes ago`
|
||||
if (lt(delta, { hours: 2 })) return `1 hour ago`
|
||||
if (lt(delta, { days: 1 })) return `${delta.hours} hours ago`
|
||||
if (lt(delta, { days: 2 })) return `yesterday`
|
||||
if (lt(delta, { days: 7 })) return `${delta.round({ smallestUnit: 'days' }).days} days ago`
|
||||
return `at ${time.toPlainDate().toString()} ${time.toPlainTime().toString({ smallestUnit: 'minutes' })}`
|
||||
const date = datetime.toPlainDate().toString();
|
||||
const time = datetime.toPlainTime().toString({ smallestUnit: 'minutes' });
|
||||
return `at ${date} ${time}`
|
||||
}
|
||||
|
||||
export function parseTime(string: string): Temporal.ZonedDateTime {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
|
||||
}
|
||||
@@ -12,11 +12,18 @@
|
||||
"editor.wordWrap": "bounded",
|
||||
"editor.wordWrapColumn": 80,
|
||||
"editor.lineNumbers": "off",
|
||||
"editor.rulers": [],
|
||||
"editor.quickSuggestions": {
|
||||
"comments": "off",
|
||||
"strings": "off",
|
||||
"other": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"recommendations": [
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"svelte.svelte-vscode"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user