Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| 741a1b6429 |
29 changed files with 578 additions and 611 deletions
53
@s
Normal file
53
@s
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
title: "Replacing garage guttering"
|
||||
slug: /replacing-garage-guttering/
|
||||
date: 2025-12-06
|
||||
tags: ["projects", "DIY"]
|
||||
---
|
||||
|
||||
I faced the following problems with the guttering on my garage:
|
||||
|
||||
- It had been bent out of shape by the wind
|
||||
- The seals on the joins had worn away causing leaks
|
||||
- There was sitting water that wasn't making it to the downpipe
|
||||
- It was old and gross
|
||||
|
||||
Whenever it rained heavily, these problems would compound and lead to water
|
||||
pouring over onto the garage brickwork.
|
||||
|
||||
The sitting water was caused by the lack of a sufficient drop from the union
|
||||
join with my neighbour's gutter to the downpipe. A further impediment was that
|
||||
the water had to turn a 90 degree angle, around the side of the garabe, before
|
||||
reaching the downpipe. As a result, water was only making it to the downpipe
|
||||
when there was very heavy rain and/or high winds. During normal drip-drainage of
|
||||
the daily dew condensation on the roof, the water was just pooling in the
|
||||
gutter.
|
||||
|
||||
I decided to redesign the passage to the downpipe. Instead of trying to make the
|
||||
water turn a bend I thought it would be better to work _with_ gravity and have
|
||||
the drop start at the end of the guttering, not around the corner. This way, the
|
||||
water would have increased velocity at the beginning of its descent into the
|
||||
downpipe.
|
||||
|
||||
By fashioning a "swans neck" sequence of joins, the downpipe now turns the
|
||||
corner _during_ descent and is fed downwards along the wall to the water butt.
|
||||
|
||||
This has been working very well and the water no longer pools. I've noticed
|
||||
however that condensation forms on the underside of the downpipe. This doesn't
|
||||
look great and I worry about it wearing away the sealant I have applied at the
|
||||
joins.
|
||||
|
||||
Overall, however I think it looks much neater as well as being more satisfying
|
||||
from an engineering perspective. The white half-round gutters blend in nicely
|
||||
with the neighbours' and look a lot cleaner.
|
||||
|
||||
I didn't keep track of costs for this project. I think in total it cost around
|
||||
£80. This included the cost of the Floplast guttering and fixtures, the downpipe
|
||||
and the protective mesh I applied to the top of the gutters to prevent blockages
|
||||
from leaves and roof moss.
|
||||
|
||||
In order to drill the fixtures into the garage masonry I needed a more powerful
|
||||
drill than my 18V battery-powered Erbauer. I bought this a few years ago before
|
||||
I knew much about brands and power-tool quality. So I bought a wired Makita
|
||||
hammer-drill for around £90. I think when I buy new power-tools in future I will
|
||||
stick to Makita. The build quality and performance is excellent.
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -80,6 +80,7 @@
|
|||
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
|
|
@ -2262,6 +2263,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.7.tgz",
|
||||
"integrity": "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.7"
|
||||
},
|
||||
|
|
@ -2417,6 +2419,7 @@
|
|||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -2529,6 +2532,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001716",
|
||||
"electron-to-chromium": "^1.5.149",
|
||||
|
|
@ -2947,6 +2951,7 @@
|
|||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -4381,6 +4386,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -4390,6 +4396,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
|
|
@ -4807,6 +4814,7 @@
|
|||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -4996,6 +5004,7 @@
|
|||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
|
|
@ -5084,6 +5093,7 @@
|
|||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
---
|
||||
title: "Amateur radio licence"
|
||||
slug: /amateur-radio-licence/
|
||||
date: 2026-03-12
|
||||
tags: ["radio"]
|
||||
---
|
||||
|
||||
In February I bit the bullet and booked the exam for the UK Amateur Radio
|
||||
Licence (Foundation Level).
|
||||
|
||||
Having the exam in the calendar helped me concentrate and remain focused, as my
|
||||
learning style is naturally quite slow and meandering. I really wanted to earn
|
||||
my licence so that I could get on the air and begin to advance my practical
|
||||
knowledge.
|
||||
|
||||
I used the official Radio Society of Great Britain (RSGB) textbook and a
|
||||
simplified syllabus from [Essex Ham Train](https://www.essexham.co.uk/train/).
|
||||
|
||||

|
||||
|
||||
In the mocks, I was averaging an 80% pass rate but in the exam itself I did
|
||||
particularly well and scored 92%, getting only two questions wrong (one of which
|
||||
didn't come up at all in the training materials). I completed the exam remotely
|
||||
but there was an RSGB invigilator observing my screen and environment to check I
|
||||
wasn't cheating! (Thank you Malcolm, G3ZNU.)
|
||||
|
||||
Having received my certificate I was able to apply to Ofcom for my callsign. I
|
||||
chose **M7SYO**. The first two characters are mandatory and denote my
|
||||
qualification level ("Foundation") and the last three are your personal choice,
|
||||
based on what is available. Luckily I was able to choose letters reflecting
|
||||
"Systems Obscure". When appropriate to do so, I can add "E" as my regional
|
||||
secondary locator to indicate that I am operating from England (e.g. ME7SYO).
|
||||
|
||||
The Foundation level is pretty generous given that it is the entry-level. I can
|
||||
transmit up to 25W and am permitted to use most of the amateur bands (10m, 12m,
|
||||
15m, 17m, 20m, 30m, 40m, 80m) at HF, apart from 60m, and many of the VHF/UHF
|
||||
bands.
|
||||
|
||||
I am lucky that my uncle, Greg, is an experienced amateur operator and secretary
|
||||
of his local amateur radio club in Macclesfield. He has been helping me with my
|
||||
practical knowledge and advising on what gear to start with. For now, I have
|
||||
ordered the affordable and well-regarded
|
||||
[Baofeng UV-5RM Plus](https://www.baofengradio.com/products/uv-5rm-plus-8w-multi-band-radio)
|
||||
handheld along with a NA-771 antenna. This will enable me to connect to
|
||||
repeaters and hopefully make my first QSOs (contacts). I also plan to construct
|
||||
some home-made antennas that I can attach to the handheld and experiment with.
|
||||
|
||||

|
||||
|
||||
As I'm still waiting for the radio to arrive, I am currently limited to
|
||||
monitoring transmissions via my
|
||||
[SDR](https://en.wikipedia.org/wiki/Software-defined_radio#Amateur_and_home_use)
|
||||
(RTL-SDR v.4). I've been able to pick up some Morse conversations and a faint
|
||||
read of the strange - possibly Iranian - number station that is currently
|
||||
[perplexing the amateur community](https://youtu.be/ErmbTpxAM7Q?si=auxTnY8HSSnu1xZ5).
|
||||
|
||||
In order to access the HF band and reach contacts further afield by exploiting
|
||||
[ionospheric propagation](https://www.qsl.net/4x4xm/Propagation/Ionosphere-propagation-of-radio-waves.htm),
|
||||
I will need a better and more powerful tranceiver. I am looking into getting a
|
||||
second-hand portable Yaesu through one of Greg's contacts. My plan is to drive
|
||||
or cycle to good spots locally and work in the field. I also eventually want to
|
||||
communicate over CW using Morse code and experiment with some radio astronomy.
|
||||
The possibilities are pretty limitless with this hobby!
|
||||
|
||||

|
||||
|
||||
To facilitate my field work I wanted a small and robust laptop so I bought a
|
||||
second hand ThinkPad X1 Carbon (Gen.9). It's very sleek compared to my T15 which
|
||||
I mainly use as a desktop device. I'm running Fedora Workstation for a
|
||||
hassle-free and easily maintainable Linux environment.
|
||||
|
||||
Finally, I joined the RSGB. This gives me access to lots of useful radio
|
||||
resources, free access to the
|
||||
[National Radio Center](https://rsgb.org/main/about-us/national-radio-centre-gb3rs/)
|
||||
at Bletchley Park, and the print edition of the monthly _RadCom_ magazine which
|
||||
is a really good read.
|
||||
|
||||
Once I start transmitting I will report here on my progress...
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 565 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 483 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 467 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -13,7 +13,7 @@ const renderer = {
|
|||
}
|
||||
|
||||
const highlighter = await createHighlighter({
|
||||
themes: ["light-plus"],
|
||||
themes: ["gruvbox-dark-hard"],
|
||||
|
||||
langs: [
|
||||
"javascript",
|
||||
|
|
@ -49,7 +49,7 @@ const posts = files.map((file) => {
|
|||
(match, lang, code) => {
|
||||
return highlighter.codeToHtml(code.trim(), {
|
||||
lang: lang || "text",
|
||||
theme: "light-plus",
|
||||
theme: "gruvbox-dark-hard",
|
||||
|
||||
transformers: [transformerColorizedBrackets()],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,57 +3,57 @@ import eolasApi from "@/api/eolas-api"
|
|||
import { convertDate } from "@/utils/convertDate"
|
||||
|
||||
const EolasEntries = ({ entries }) => {
|
||||
return (
|
||||
entries &&
|
||||
entries.map((entry, i) => (
|
||||
<ul>
|
||||
<li className="mb-4">
|
||||
<div className="flex justify-between items-center relative gap-3 hover:bg-[#504945]">
|
||||
<span className="overflow-hidden whitespace-nowrap text-muted-foreground shrink-0 condensed">
|
||||
{convertDate(entry.last_modified)}
|
||||
</span>
|
||||
<a
|
||||
href={`https://eolas.systemsobscure.net/entries/${entry.title}`}
|
||||
key={i}
|
||||
className="text-right overflow-hidden text-ellipsis whitespace-nowrap min-w-0 flex-1 "
|
||||
>
|
||||
{entry.title.replace(/_/g, " ")}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
))
|
||||
)
|
||||
return (
|
||||
entries &&
|
||||
entries.map((entry, i) => (
|
||||
<ul>
|
||||
<li className="mb-4">
|
||||
<div className="flex justify-between items-center relative gap-3 hover:bg-[#504945]">
|
||||
<span className="overflow-hidden whitespace-nowrap text-muted-foreground shrink-0 condensed">
|
||||
{convertDate(entry.last_modified)}
|
||||
</span>
|
||||
<a
|
||||
href={`https://eolas.systemsobscure.net/entries/${entry.title}`}
|
||||
key={i}
|
||||
className="text-right overflow-hidden text-ellipsis whitespace-nowrap min-w-0 flex-1 "
|
||||
>
|
||||
{entry.title.replace(/_/g, " ")}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
const EolasListing = () => {
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: [`eolas_listing`],
|
||||
queryFn: () =>
|
||||
eolasApi.get(`entries?limit=5&sort=date`).then((res) => res.data),
|
||||
})
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: [`eolas_listing`],
|
||||
queryFn: () =>
|
||||
eolasApi.get(`entries?limit=5&sort=date`).then((res) => res.data),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-4 px-1 md:p-4 grow">
|
||||
<div className="space-my-8">
|
||||
<section className="container">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-[#d65d0e]! h2-home scanlined px-2">
|
||||
{`eolas recent`}
|
||||
</h2>
|
||||
return (
|
||||
<div className="container mx-auto py-4 px-1 md:p-4 grow">
|
||||
<div className="space-my-8">
|
||||
<section className="container">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-[#d65d0e]! h2-home">
|
||||
{`Recent learning`}
|
||||
</h2>
|
||||
|
||||
{isLoading && <div>Loading...</div>}
|
||||
{isLoading && <div>Loading...</div>}
|
||||
|
||||
{error ? (
|
||||
<div className="border-l-2 border-[#cc241d] px-3 bg-[#cc241d]/20">
|
||||
Error fetching recent learning
|
||||
</div>
|
||||
) : (
|
||||
<EolasEntries entries={data?.data} />
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
{error ? (
|
||||
<div className="border-l-2 border-[#cc241d] px-3 bg-[#cc241d]/20">
|
||||
Error fetching recent learning
|
||||
</div>
|
||||
) : (
|
||||
<EolasEntries entries={data?.data} />
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EolasListing
|
||||
|
|
|
|||
|
|
@ -1,35 +1,29 @@
|
|||
import MetricBar from "./MetricBar"
|
||||
|
||||
const LanguagesChart = ({ chartData, error }) => {
|
||||
return (
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<div
|
||||
className="code-stat-label"
|
||||
style={{
|
||||
paddingBottom: ".5rem",
|
||||
marginBottom: ".5rem",
|
||||
}}
|
||||
>
|
||||
Programming languages
|
||||
</div>
|
||||
return (
|
||||
<div className="bg-sidebar p-3 my-4 mx-2 rounded-2xl">
|
||||
<div className="text-muted-foreground text-sm pb-2">
|
||||
programming languages
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div>Data could not be found!</div>
|
||||
) : !chartData?.length ? (
|
||||
<div>No data for time period.</div>
|
||||
) : (
|
||||
chartData.map((x) => (
|
||||
<MetricBar
|
||||
key={x.language}
|
||||
metric={x.language}
|
||||
hours={x.hours}
|
||||
percentage={x.percentage}
|
||||
color="black"
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
{error ? (
|
||||
<div>Data could not be found!</div>
|
||||
) : !chartData?.length ? (
|
||||
<div>No data for time period.</div>
|
||||
) : (
|
||||
chartData.map((x) => (
|
||||
<MetricBar
|
||||
key={x.language}
|
||||
metric={x.language}
|
||||
hours={x.hours}
|
||||
percentage={x.percentage}
|
||||
color="#458588"
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LanguagesChart
|
||||
|
|
|
|||
|
|
@ -1,36 +1,37 @@
|
|||
const MetricBar = ({ metric, hours, percentage, color }) => (
|
||||
<div style={{ marginBottom: "12px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "4px",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
<span style={{}}>{metric}</span>
|
||||
<span style={{ color: "black" }}>
|
||||
{hours}h ({percentage}%)
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "8px",
|
||||
backgroundColor: "lightgrey",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
height: "100%",
|
||||
backgroundColor: color,
|
||||
transition: "width 0.3s ease",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: "12px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "4px",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
<span style={{}}>{metric}</span>
|
||||
<span style={{ color: "#bdae93" }}>
|
||||
{hours}h ({percentage}%)
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "8px",
|
||||
backgroundColor: "#32302f",
|
||||
borderRadius: "4px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
height: "100%",
|
||||
backgroundColor: color,
|
||||
transition: "width 0.3s ease",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default MetricBar
|
||||
|
|
|
|||
|
|
@ -1,38 +1,30 @@
|
|||
import MetricBar from "./MetricBar"
|
||||
|
||||
const ProjectsChart = ({ chartData, error }) => {
|
||||
return (
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
<div
|
||||
className="code-stat-label"
|
||||
style={{
|
||||
paddingBottom: ".5rem",
|
||||
marginBottom: ".5rem",
|
||||
}}
|
||||
>
|
||||
Repos
|
||||
</div>
|
||||
return (
|
||||
<div className="bg-sidebar p-3 my-4">
|
||||
<div className="text-muted-foreground text-sm pb-2">projects</div>
|
||||
|
||||
{error ? (
|
||||
<div>Data could not be found!</div>
|
||||
) : !chartData?.length ? (
|
||||
<div>No data for time period.</div>
|
||||
) : (
|
||||
chartData.map((x) => (
|
||||
<MetricBar
|
||||
key={x.project}
|
||||
metric={x.project}
|
||||
hours={x.hours}
|
||||
percentage={x.percentage}
|
||||
color="black"
|
||||
/>
|
||||
))
|
||||
)}
|
||||
<div className="code-stats-disclaimer">
|
||||
Data excludes workplace repos.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
{error ? (
|
||||
<div>Data could not be found!</div>
|
||||
) : !chartData?.length ? (
|
||||
<div>No data for time period.</div>
|
||||
) : (
|
||||
chartData.map((x) => (
|
||||
<MetricBar
|
||||
key={x.project}
|
||||
metric={x.project}
|
||||
hours={x.hours}
|
||||
percentage={x.percentage}
|
||||
color="#fe8019"
|
||||
/>
|
||||
))
|
||||
)}
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Data excludes workplace repos.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectsChart
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
const Scorecard = ({ title, metric }) => {
|
||||
return (
|
||||
<div className="code-stat-grid">
|
||||
<div className="code-stat-label">{title}</div>
|
||||
<div className="code-stat-metric">{metric}</div>
|
||||
<div className="bg-sidebar p-3 rounded-2xl">
|
||||
<div className="text-muted-foreground text-sm">{title}</div>
|
||||
<div className="text-lg">{metric}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
import wakapiApi from "../api/wakapi-api"
|
||||
import { convertDateFriendly } from "../utils/convertDate"
|
||||
import Scorecard from "../components/Scorecard"
|
||||
|
|
@ -33,9 +32,7 @@ const CodeStats = () => {
|
|||
data &&
|
||||
data?.projects.filter(
|
||||
(project) =>
|
||||
!project.key.includes("gp-") &&
|
||||
!project.key.includes("unknown") &&
|
||||
!project.key.includes("amber")
|
||||
!project.key.includes("gp-") && !project.key.includes("unknown")
|
||||
)
|
||||
|
||||
const personalProjectsSorted =
|
||||
|
|
@ -70,52 +67,45 @@ const CodeStats = () => {
|
|||
.slice(0, 4)
|
||||
|
||||
return (
|
||||
<div className="code-stats-sect">
|
||||
<div className="">
|
||||
<section className="">
|
||||
<div className="">
|
||||
<h2 className="big-title">{`Code this month`}</h2>
|
||||
|
||||
{/*
|
||||
<table
|
||||
border="1"
|
||||
width="100%"
|
||||
style={{ borderCollapse: "collapse" }}
|
||||
>
|
||||
<tr>
|
||||
<td style={{ paddingLeft: "4px" }}>Period:</td>
|
||||
<td style={{ paddingLeft: "4px" }}>
|
||||
{convertDateFriendly(data?.from)} -{" "}
|
||||
{convertDateFriendly(data?.to)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ paddingLeft: "4px" }}>Total time:</td>
|
||||
<td style={{ paddingLeft: "4px" }}>
|
||||
{error
|
||||
? "Error"
|
||||
: isLoading
|
||||
? "Loading..."
|
||||
: grandTotalFormatted}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ paddingLeft: "4px" }}>Main project:</td>
|
||||
<td style={{ paddingLeft: "4px" }}>
|
||||
{error ? "Error" : isLoading ? "Loading..." : mainProject}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
*/}
|
||||
<div className="container mx-auto py-4 px-1 md:p-4 grow ">
|
||||
<div className="space-my-8">
|
||||
<section className="container">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center md:justify-between">
|
||||
<h2 className="text-3xl font-semibold mb-4 text-[#458588]! h2-home">
|
||||
{`code stats`}
|
||||
</h2>
|
||||
<div className="mb-4 text-sm text-muted">
|
||||
{convertDateFriendly(data?.from)} -{" "}
|
||||
{convertDateFriendly(data?.to)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mx-2">
|
||||
<Scorecard
|
||||
title="time coding"
|
||||
metric={
|
||||
error ? "Error" : isLoading ? "Loading..." : grandTotalFormatted
|
||||
}
|
||||
/>
|
||||
|
||||
<Scorecard
|
||||
title="main project"
|
||||
metric={error ? "Error" : isLoading ? "Loading..." : mainProject}
|
||||
/>
|
||||
|
||||
<Scorecard
|
||||
title="OS"
|
||||
metric={error ? "Error" : isLoading ? "Loading..." : osMetric}
|
||||
/>
|
||||
</div>
|
||||
<LanguagesChart chartData={languagesChartData} error={error} />
|
||||
|
||||
<ProjectsChart chartData={projectsChartData} error={error} />
|
||||
<div className="code-stats-disclaimer">
|
||||
<div className="text-sm text-center text-muted-foreground">
|
||||
Data sourced from my self-hosted{" "}
|
||||
<a href="https://wakapi.dev/" target="__blank" className="">
|
||||
<a
|
||||
href="https://wakapi.dev/"
|
||||
target="__blank"
|
||||
className="underline decoration-1 hover:text-primary/80 underline-offset-4 text-primary"
|
||||
>
|
||||
Wakapi
|
||||
</a>{" "}
|
||||
instance.
|
||||
|
|
|
|||
|
|
@ -4,34 +4,40 @@ import { Link } from "react-router"
|
|||
import { convertDate } from "@/utils/convertDate"
|
||||
|
||||
const PostListing = ({ posts, title, showAllButton }) => {
|
||||
return (
|
||||
<div className="">
|
||||
<div className="">
|
||||
<section className="">
|
||||
<h2 className="big-title">{`${title}`}</h2>
|
||||
{posts.map((post) => (
|
||||
<ul className="no-bullets">
|
||||
<li className="">
|
||||
<div className="post-listing-item">
|
||||
<span style={{ marginRight: "1rem" }}>
|
||||
{convertDate(post.date)}
|
||||
</span>
|
||||
<Link to={`/posts/${post.slug}`} key={post.slug} className="">
|
||||
{post.title}
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="container mx-auto py-4 px-1 md:p-4 grow">
|
||||
<div className="space-my-8">
|
||||
<section className="container">
|
||||
<h2 className="text-3xl font-semibold mb-4 block h2-home">
|
||||
{`${title}`}
|
||||
</h2>
|
||||
{posts.map((post) => (
|
||||
<ul>
|
||||
<li className="mb-4">
|
||||
<div className="flex justify-between flex-col relative hover:bg-[#504945] hover:rounded-2xl p-2">
|
||||
<span className="overflow-hidden whitespace-nowrap text-muted-foreground shrink-0 condensed">
|
||||
{convertDate(post.date)}
|
||||
</span>
|
||||
<Link
|
||||
to={`/posts/${post.slug}`}
|
||||
key={post.slug}
|
||||
className="overflow-hidden text-ellipsis whitespace-nowrap min-w-0"
|
||||
>
|
||||
{post.title}
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PostListing
|
||||
{
|
||||
/*
|
||||
/*
|
||||
|
||||
<Link
|
||||
to={`/posts/${post.slug}`}
|
||||
|
|
|
|||
211
src/index.css
211
src/index.css
|
|
@ -1,149 +1,100 @@
|
|||
@media (min-width: 768px) {
|
||||
main {
|
||||
max-width: 768px;
|
||||
margin: 0 1.5rem;
|
||||
}
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap");
|
||||
@import "./styles/_variables.css";
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
.post-listing-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.post-listing-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title-link {
|
||||
letter-spacing: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.post-listing-item {
|
||||
display: flex;
|
||||
* {
|
||||
outline-color: color-mix(in srgb, var(--ring) 50%, transparent);
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: "DejaVu Sans", sans-serif;
|
||||
font-family: var(--font-sansserif);
|
||||
}
|
||||
|
||||
.header-links {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
gap: 1rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.plain-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.no-bullets {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-style: italic;
|
||||
.condensed {
|
||||
font-family: "IBM Plex Sans";
|
||||
}
|
||||
|
||||
figcaption {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: "IBM Plex Sans";
|
||||
}
|
||||
|
||||
figure {
|
||||
text-align: center;
|
||||
h1 {
|
||||
color: var(--color-orange-light);
|
||||
font-family: "IBM Plex Sans";
|
||||
}
|
||||
|
||||
figure img {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
min-width: 300px;
|
||||
height: auto;
|
||||
h2 {
|
||||
font-family: "IBM Plex Sans";
|
||||
color: var(--color-green-light);
|
||||
}
|
||||
|
||||
.h2-home {
|
||||
font-family: "IBM Plex Sans";
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: "IBM Plex Sans";
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.monospaced-font {
|
||||
font-family: "iA Writer Mono";
|
||||
}
|
||||
|
||||
.scanlined {
|
||||
position: relative;
|
||||
/* Add this */
|
||||
}
|
||||
|
||||
.scanlined::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.4) 1px, transparent 1px);
|
||||
background-size: 2px 2px;
|
||||
background-repeat: repeat;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
/* Might want to lower this too */
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: var(--font-monospaced);
|
||||
}
|
||||
|
||||
p,
|
||||
li {
|
||||
font-size: 1.125rem !important;
|
||||
line-height: 1.8 !important;
|
||||
}
|
||||
|
||||
p code {
|
||||
color: var(--foreground);
|
||||
background: #504945;
|
||||
font-size: 14px;
|
||||
padding: 0.2rem 0.3rem;
|
||||
border-radius: var(--radius);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.shiki {
|
||||
padding: 1rem;
|
||||
/* border: 1pt solid black; */
|
||||
background-color: whitesmoke !important;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.code-stats-sect {
|
||||
font-family: sans-serif !important;
|
||||
}
|
||||
|
||||
.code-stat-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.code-stat-label {
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.code-stats-disclaimer {
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about-li-padding {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
h1.site-title,
|
||||
h1.post-title {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.big-title {
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.blog-meta {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "DejaVu Sans";
|
||||
src: url("/fonts/DejaVuSans.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "DejaVu Sans";
|
||||
src: url("/fonts/DejaVuSans-Bold.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "DejaVu Sans";
|
||||
src: url("/fonts/DejaVuSans-Oblique.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "DejaVu Sans";
|
||||
src: url("/fonts/DejaVuSans-BoldOblique.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: 0;
|
||||
overflow-x: auto;
|
||||
margin: 1.5rem 0;
|
||||
line-height: 1.3;
|
||||
/* counter-reset: line; */
|
||||
font-family: var(--font-monospaced) !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,16 +4,16 @@ import portrait from "../images/portrait-compressed.jpg"
|
|||
const AboutPage = () => {
|
||||
return (
|
||||
<MainTemplate>
|
||||
<div className="">
|
||||
<figure className="">
|
||||
<div className="">
|
||||
<div className="container mx-auto py-4 px-1 md:p-4">
|
||||
<figure className="w-full flex flex-col items-center mb-6">
|
||||
<div className="scanlined inline-block">
|
||||
<img
|
||||
alt="A portrait of the blog author"
|
||||
src={portrait}
|
||||
style={{ width: '25%' }}
|
||||
className="w-80 flex"
|
||||
/>
|
||||
</div>
|
||||
<figcaption className="">
|
||||
<figcaption className="text-sm text-muted-foreground mt-3 text-center">
|
||||
Pictured with the WITCH computer at the{" "}
|
||||
<a
|
||||
href="https://www.tnmoc.org/"
|
||||
|
|
@ -57,33 +57,29 @@ const AboutPage = () => {
|
|||
>
|
||||
Arria NLG
|
||||
</a>{" "}
|
||||
and in several web developer roles. Before software, I was a
|
||||
and in several web developer roles. Before software I was a
|
||||
teacher.{" "}
|
||||
</p>
|
||||
|
||||
<p>I am licenced amateur radio operator. My callsign is ME7SYO.</p>
|
||||
|
||||
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
|
||||
Some things I like:
|
||||
<ul className="no-bullets">
|
||||
<li className=""><span className="about-li-padding">🐶</span>Staffies and other bull-breeds</li>
|
||||
<li className=""><span className="about-li-padding">🎼</span>Classical music (Haydn, Mozart, JSB)</li>
|
||||
<li className=""><span className="about-li-padding">🛸</span>Science fiction </li>
|
||||
<li className=""><span className="about-li-padding">🐦⬛</span>Bird watching</li>
|
||||
<ul className="pt-2">
|
||||
<li className="mb-1">🐶 Staffies and other bull-breeds</li>
|
||||
<li className="mb-1">🎼 Classical music (Haydn, Mozart, JSB)</li>
|
||||
<li className="mb-1">🛸 Science fiction </li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p className="">
|
||||
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
|
||||
Some things I'm interested in:
|
||||
<ul className="no-bullets">
|
||||
<li className=""><span className="about-li-padding">🧑💻</span>Self-hosting and digital resiliance</li>
|
||||
|
||||
|
||||
<li className=""><span className="about-li-padding">🖥️</span>The history of computing and networks</li>
|
||||
<li className=""><span className="about-li-padding">☸️</span>Buddhism</li>
|
||||
<li className=""><span className="about-li-padding">📡</span>Amateur radio</li>
|
||||
<li className=""><span className="about-li-padding">👽</span>SETI</li>
|
||||
<ul className="pt-2">
|
||||
<li className="mb-1">🧑💻 Self-hosting and digital resiliance</li>
|
||||
<li className="mb-1">🖳 The history of computing and networks</li>
|
||||
<li className="mb-1">☸️ Buddhism</li>
|
||||
{/*
|
||||
|
||||
<li className="mb-1">📡 Civil communications infrastructure</li>
|
||||
*/}
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,23 +4,70 @@ import { usePosts } from "@/hooks/usePosts"
|
|||
import gruvboxComputer from "../images/gruvbox-computer.svg"
|
||||
import EolasListing from "@/components/EolasListing"
|
||||
import CodeStats from "../containers/CodeStats"
|
||||
import { Link } from "react-router"
|
||||
|
||||
const HomePage = () => {
|
||||
const { posts } = usePosts()
|
||||
return (
|
||||
<MainTemplate>
|
||||
<p>
|
||||
A wizard who goes to bed early. This is <Link to="/about">my</Link>{" "}
|
||||
technical scrapbook and digital garden.
|
||||
</p>
|
||||
<div className="container mx-auto md:p-4 py-4 px-1 grow">
|
||||
<div className="space-my-8">
|
||||
<section className="space-y-4">
|
||||
<div className="gap-6 flex flex-col items-center sm:flex-row">
|
||||
<div className="">
|
||||
<img
|
||||
src={gruvboxComputer}
|
||||
className="md:w-80 w-50 rounded-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-5xl font-bold py-3 text-center sm:text-left md:text-left">
|
||||
<div className="">systems obscure</div>
|
||||
</h1>
|
||||
<p className="text-center sm:text-left md:text-left text-muted font-medium text-lg mt-1">
|
||||
A wizard who goes to bed early. This is my technical scrapbook
|
||||
and digital garden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PostListing title="Recent posts" posts={posts.slice(0, 5)} />
|
||||
<PostListing title="recent posts" posts={posts.slice(0, 5)} />
|
||||
|
||||
<PostListing
|
||||
title="Highlights"
|
||||
title="highlights"
|
||||
posts={posts.filter((post) => post.tags.includes("highlight"))}
|
||||
/>
|
||||
|
||||
<div className="container mx-auto md:p-4 py-4 px-1 grow mb-4">
|
||||
<div className="">
|
||||
<section className="container">
|
||||
<h2 className="text-3xl font-semibold mb-4 text-[#d3869b]! h2-home">
|
||||
{`projects`}
|
||||
</h2>
|
||||
<ul className="p-2">
|
||||
<li className="pb-2">
|
||||
<a
|
||||
className="underline underline-offset-4 text-primary hover:text-primary/80 font-medium text-xl"
|
||||
href="https://eolas.systemsobscure.net"
|
||||
target="_blank"
|
||||
>
|
||||
eolas
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<p className="">
|
||||
A public frontend for my local Zettelkasten created with
|
||||
NodeJS, Python and React.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<CodeStats />
|
||||
<EolasListing />
|
||||
</MainTemplate>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const PostsPage = () => {
|
|||
|
||||
return (
|
||||
<MainTemplate>
|
||||
<PostListing title="All posts" posts={posts} />
|
||||
<PostListing title="all posts" posts={posts} />
|
||||
</MainTemplate>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +1,79 @@
|
|||
:root {
|
||||
--radius: 0.3rem;
|
||||
--background: #282828;
|
||||
--foreground: #ebdbb2;
|
||||
--sidebar: #3c3836;
|
||||
--color-red-light: #fb4934;
|
||||
--color-orange-light: #fe8019;
|
||||
--color-green-light: #b8bb26;
|
||||
--color-aqua-muted: #689d6a;
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: #8ec07c;
|
||||
--primary-muted: #689d6a;
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: #bdae93;
|
||||
--muted-foreground: #928374;
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--font-monospaced: "Jetbrains Mono";
|
||||
--font-sansserif: "Inter", sans-serif;
|
||||
--radius: 0.3rem;
|
||||
--background: #282828;
|
||||
--foreground: #fbf1c7;
|
||||
--sidebar: #3c3836;
|
||||
--color-red-light: #fb4934;
|
||||
--color-orange-light: #fe8019;
|
||||
--color-green-light: #b8bb26;
|
||||
--color-aqua-muted: #689d6a;
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: #8ec07c;
|
||||
--primary-muted: #689d6a;
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: #bdae93;
|
||||
--muted-foreground: #928374;
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--font-monospaced: "Jetbrains Mono";
|
||||
--font-sansserif: "IBM Plex Sans", sans-serif;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius));
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius));
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,57 +3,75 @@ import MainTemplate from "./MainTemplate"
|
|||
import { Link, useParams } from "react-router"
|
||||
import { convertDate } from "@/utils/convertDate"
|
||||
import { usePosts } from "@/hooks/usePosts"
|
||||
import { convertDateFriendly } from "../utils/convertDate"
|
||||
|
||||
const BlogTemplate = () => {
|
||||
const { slug } = useParams()
|
||||
const { posts } = usePosts()
|
||||
const post = posts?.find((x) => x.slug === slug)
|
||||
const { slug } = useParams()
|
||||
const { posts } = usePosts()
|
||||
const post = posts?.find((x) => x.slug === slug)
|
||||
|
||||
return (
|
||||
<MainTemplate>
|
||||
<div className="">
|
||||
{!post ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<article className="">
|
||||
<header
|
||||
className="big-title"
|
||||
style={{ paddingBottom: "1rem", marginBottom: "2rem" }}
|
||||
>
|
||||
<h1 className="post-title">{post?.title}</h1>
|
||||
<div
|
||||
className="blog-meta"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "1rem",
|
||||
}}
|
||||
>
|
||||
<time datetime={convertDate(post?.date)} className="text-sm">
|
||||
{convertDateFriendly(post?.date)}
|
||||
</time>
|
||||
<div className="">
|
||||
<span>Tag(s): </span>
|
||||
{post?.tags?.map((tag, i) => (
|
||||
<Link
|
||||
style={{ marginRight: "0.5rem" }}
|
||||
key={i}
|
||||
to={`/tags/${tag}`}
|
||||
>
|
||||
{tag}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
return (
|
||||
<MainTemplate>
|
||||
<div className="container mx-auto py-4 px-1 md:p-4 grow">
|
||||
{!post ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<article className="prose prose-lg max-w-none">
|
||||
<header className="mb-6 pb-4">
|
||||
<h1 className="text-5xl font-bold mb-4 inline-block">
|
||||
{post?.title}
|
||||
</h1>
|
||||
<div className="flex flex-wrap align-center gap-4 text-[#928374] condensed font-medium">
|
||||
<time datetime={convertDate(post?.date)} className="">
|
||||
{convertDate(post?.date)}
|
||||
</time>
|
||||
<div className="flex flex-wrap gap-3 align-center">
|
||||
{post?.tags?.map((tag, i) => (
|
||||
<Link
|
||||
className="text-primary underline underline-offset-3 hover:text-[#689d6a]"
|
||||
key={i}
|
||||
to={`/tags/${tag}`}
|
||||
>
|
||||
{tag}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div dangerouslySetInnerHTML={{ __html: post?.html }} />
|
||||
</article>
|
||||
)}
|
||||
</div>
|
||||
</MainTemplate>
|
||||
)
|
||||
<div
|
||||
className="
|
||||
[&>h2]:text-2xl [&>h2]:font-bold [&>h2]:my-4 [&>h2]:text-[#ebdbb2]!
|
||||
[&>h3]:text-xl [&>h3]:font-bold [&>h3]:my-4 [&>h3]:text-[#ebdbb2]!
|
||||
[&>h4]:text-lg [&>h4]:font-bold [&>h4]:my-4 [&>h4]:text-[#ebdbb2]!
|
||||
[&>p]:leading-7 [&>p:not(:first-child)]:mt-4
|
||||
[&>p+:is(h1,h2,h3,h4,h5,h6)]:mt-6
|
||||
[&>blockquote]:mt-4 [&>blockquote]:border-l-2 [&>blockquote]:pl-6 [&>blockquote]:text-muted-foreground
|
||||
[&>ul]:my-4 [&>ul]:ml-6 [&>ul]:list-disc [&>ul>li]:mt-2
|
||||
[&>pre]:mt-6 [&>pre]:mb-6
|
||||
[&>p+pre]:mt-6
|
||||
[&>pre+p]:mt-6
|
||||
[&>ul+pre]:mt-6
|
||||
[&>li]:leading-[1.6]
|
||||
[&_li_code]:relative [&_li_code]:rounded [&_li_code]:bg-[#504945] [&_li_code]:px-[0.3rem] [&_li_code]:py-[0.2rem] [&_li_code]:font-mono [&_li_code]:text-sm [&_li_code]:font-normal
|
||||
[&>code]:relative [&>code]:rounded [&>code]:bg-[#504945] [&>code]:px-[0.3rem] [&>code]:py-[0.2rem] [&>code]:font-mono [&>code]:text-sm [&>code]:font-normal
|
||||
[&>figure]:w-full [&>figure]:max-w-2xl [&>figure]:flex [&>figure]:flex-col [&>figure]:items-center [&>figure]:justify-center [&>figure]:mb-6 [&>figure]:mx-auto [&>figure>img]:max-w-full [&>figure>img]:max-h-[700px] [&>figure>img]:w-auto [&>figure>img]:h-auto [&>figure>img]:object-contain
|
||||
[&>figure>img]:max-w-2xl [&>figure>img]:max-h-[700px] [&>figure>img]:w-auto [&>figure>img]:h-auto [&>figure>img]:object-contain
|
||||
[&>figure>figcaption]:text-sm [&>figure>figcaption]:text-[#bdae93] [&>figure>figcaption]:mt-3 [&>figure>figcaption]:text-center
|
||||
[&>figure>figcaption>a]:text-primary [&>figure>figcaption>a:hover]:text-primary/80
|
||||
[&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-[#689d6a] [&_a]:text-primary
|
||||
[&>table]:w-full [&>table]:my-4
|
||||
[&>table>thead>tr]:m-0 [&>table>thead>tr]:border-t [&>table>thead>tr]:p-0 [&>table>thead>tr:even]:bg-muted
|
||||
[&>table>thead>tr>th]:border [&>table>thead>tr>th]:px-4 [&>table>thead>tr>th]:py-2 [&>table>thead>tr>th]:text-left [&>table>thead>tr>th]:font-bold [&>table>thead>tr>th[align=center]]:text-center [&>table>thead>tr>th[align=right]]:text-right
|
||||
[&>table>tbody>tr]:m-0 [&>table>tbody>tr]:border-t [&>table>tbody>tr]:p-0 [&>table>tbody>tr:even]:bg-muted
|
||||
[&>table>tbody>tr>td]:border [&>table>tbody>tr>td]:px-4 [&>table>tbody>tr>td]:py-2 [&>table>tbody>tr>td]:text-left [&>table>tbody>tr>td[align=center]]:text-center [&>table>tbody>tr>td[align=right]]:text-right
|
||||
"
|
||||
dangerouslySetInnerHTML={{ __html: post?.html }}
|
||||
/>
|
||||
</article>
|
||||
)}
|
||||
</div>
|
||||
</MainTemplate>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogTemplate
|
||||
|
|
|
|||
|
|
@ -1,44 +1,32 @@
|
|||
// @ts-nocheck
|
||||
|
||||
import gruvboxComputer from "../images/gruvbox-computer.svg"
|
||||
import { Link } from "react-router"
|
||||
const Header = () => {
|
||||
return (
|
||||
<header>
|
||||
<nav className="">
|
||||
<h1 className="site-title">
|
||||
<Link className="title-link plain-link" to="/">
|
||||
Systems Obscure
|
||||
</Link>
|
||||
</h1>
|
||||
<ul className="header-links">
|
||||
<li>
|
||||
<Link to="/">Home</Link>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Link to="/posts">Posts</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/about">About</Link>
|
||||
</li>
|
||||
<li className="flex flex-col items-center justify-center">
|
||||
<a
|
||||
className=""
|
||||
href="https://forgejo.systemsobscure.net/thomasabishop"
|
||||
target="blank"
|
||||
<header className="md:py-6 pb-4">
|
||||
<nav className="bg-sidebar container mx-auto justify-between flex gap-1 rounded-r-2xl">
|
||||
<Link to="/">
|
||||
<div className="">
|
||||
<img src={gruvboxComputer} className="w-12" />
|
||||
</div>
|
||||
</Link>
|
||||
<ul class="flex space-x-4 px-4 py-2 text-sm">
|
||||
<li class="flex flex-col items-center justify-center">
|
||||
<Link
|
||||
class="text-primary hover:text-[#689d6a] condensed font-semibold text-xl"
|
||||
to="/posts"
|
||||
>
|
||||
Code
|
||||
</a>
|
||||
posts
|
||||
</Link>
|
||||
</li>
|
||||
<li className="flex flex-col items-center justify-center">
|
||||
<a
|
||||
className=""
|
||||
href="https://fosstodon.org/@systemsobscure"
|
||||
rel="me"
|
||||
target="blank"
|
||||
<li class="flex flex-col items-center justify-center">
|
||||
<Link
|
||||
class="text-primary hover:text-[#689d6a] condensed font-semibold text-xl"
|
||||
to="/about"
|
||||
>
|
||||
Fosstodon
|
||||
</a>
|
||||
about
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
@ -48,16 +36,16 @@ const Header = () => {
|
|||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className="mx-auto">
|
||||
<nav>
|
||||
<ul className="">
|
||||
<footer className="bg-sidebar container mx-auto px-4 mt-10 mb-8 rounded-2xl p-2">
|
||||
<nav className="">
|
||||
<ul className="flex flex-row justify-start gap-4">
|
||||
<li className="flex flex-col items-center justify-center">
|
||||
<a
|
||||
className=""
|
||||
className="text-primary underline underline-offset-3 hover:text-[#689d6a] font-semibold"
|
||||
href="https://forgejo.systemsobscure.net/thomasabishop"
|
||||
target="blank"
|
||||
>
|
||||
Forgejo
|
||||
forgejo
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -68,14 +56,13 @@ const Footer = () => {
|
|||
|
||||
const MainTemplate = ({ children }) => {
|
||||
return (
|
||||
<div className="">
|
||||
<main className="">
|
||||
<div className="antialiased max-w-3xl mt-3 mx-auto bg-[#282828] no-scanlines wrapper">
|
||||
<main className="flex-auto min-w-0 mt-0 flex flex-col px-2 md:px-0">
|
||||
<Header />
|
||||
<div>{children}</div>
|
||||
<Footer />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default MainTemplate
|
||||
//antialiased max-w-3xl mt-3 mx-auto bg-[#282828] no-scanlines wrapper
|
||||
//main: flex-auto min-w-0 mt-0 flex flex-col px-2 md:px-0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue