Compare commits

...

23 Commits

Author SHA1 Message Date
dc4d564059
Breadcrumbs with navigation for mobile
All checks were successful
Build and deploy website / build (push) Successful in 38s
2025-03-16 21:32:11 +01:00
05ef06f95c
Breadcrumbs with navigation
All checks were successful
Build and deploy website / build (push) Successful in 38s
2025-03-15 15:34:24 +01:00
097850267c
Homelab and raspberry pi uses
All checks were successful
Build and deploy website / build (push) Successful in 1m0s
2025-03-10 21:44:20 +01:00
9a82eba757
📦 Updated Astro to v5.4 and DaisyUI to v5 2025-03-01 10:04:48 +01:00
ebb3db8645
Add description and keywords to meta tags
All checks were successful
Build and deploy website / build (push) Successful in 37s
2025-03-01 09:52:10 +01:00
a2584b97a1
Added Sb1 Actual integration to projects.
All checks were successful
Build and deploy website / build (push) Successful in 39s
- Code style is Catppuccin Mocha
- Added lang tag to project
- Added keywords to project
- Sort projects by latest updated
2025-02-27 21:13:01 +01:00
14c65bda05
Replaced JS Date API with dayjs
All checks were successful
Build and deploy website / build (push) Successful in 56s
2025-02-25 19:49:21 +01:00
16104d12ae
Added more links, upddated TODO 2025-02-25 19:33:01 +01:00
83b2b9ac68
📦 Update dependencies
All checks were successful
Build and deploy website / build (push) Successful in 58s
2025-02-25 19:04:17 +01:00
8cc5c6971f
Updated lockfile, replaced CMD with ENTRYPOINT
All checks were successful
Build and deploy website / build (push) Successful in 56s
2025-02-16 15:02:20 +01:00
25a38d2f0e
🔖 Replaced heroimage with an image of me, fix typo
All checks were successful
Build and deploy website / build (push) Successful in 32s
2025-02-16 13:50:27 +01:00
e1c3ae7d87
Latest projects on landing page, moved landing page components
All checks were successful
Build and deploy website / build (push) Successful in 34s
2025-02-16 12:01:51 +01:00
22cdc634f9
More Catppuccin colours, colour links by default 2025-02-16 11:49:06 +01:00
1b3c6c629e
More mobile friendly, fix wrong URL, Hamburger menu
All checks were successful
Build and deploy website / build (push) Successful in 33s
- Hamburger menu on mobile
- title is moved into header on mobile
- Smaller titles on mobile
- Fix wrong import of env in config
- Cleaned up unused imports
2025-02-15 23:00:38 +01:00
ff2f65bf59
🚑 Fix build error
All checks were successful
Build and deploy website / build (push) Successful in 32s
Signed-off-by: Martin Berg Alstad <git@martials.no>
2025-02-15 19:31:44 +01:00
54db411930
More style changes, removed test projects, favicon
Some checks failed
Build and deploy website / build (push) Failing after 22s
- Fix missing aria-labels
- Fix breadcrumbs on specific projects
- Removed uses from NavBar
- Center image on project page
- Colour link on project page
- Initial README

Signed-off-by: Martin Berg Alstad <git@martials.no>
2025-02-15 19:29:06 +01:00
023c8b7c85
Update general style
- Smaller width of page
- Center navbar
- Fonts
- Catppuccin Mocha colours
- TODO.md
- Show all uses at the same time
- and more...
2025-02-15 18:01:17 +01:00
cb00252364
🎨 Format files, Added codeberg to links,lighter colour for linkcards
All checks were successful
Build and deploy website / build (push) Successful in 33s
2025-02-15 14:59:32 +01:00
04f279dab3
👽️ Updated legacy svelte components to runes
All checks were successful
Build and deploy website / build (push) Successful in 1m7s
2025-02-15 14:34:47 +01:00
7c5b228e59
📦️ Updated Tailwind to v4, updated hardware to uses 2025-02-15 13:29:01 +01:00
cfd11a98ec
Fixed Type error
All checks were successful
Build and deploy website / build (push) Successful in 35s
2025-01-19 20:07:26 +01:00
89a349b4fd
⬆ Updated all packages to latest stable, changed hardware url to /uses
Some checks failed
Build and deploy website / build (push) Failing after 42s
2025-01-19 20:04:06 +01:00
2fc18f642d
Updated keyboard in hardware config
All checks were successful
Build and deploy website / build (push) Successful in 1m1s
2025-01-04 17:50:25 +01:00
74 changed files with 2271 additions and 1830 deletions

View File

@ -23,4 +23,4 @@ COPY --from=build /app/dist ./dist
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD node ./dist/server/entry.mjs
ENTRYPOINT node ./dist/server/entry.mjs

View File

@ -1,54 +1,5 @@
# Astro Starter Kit: Basics
# Homepage / portfolio
```sh
npm create astro@latest -- --template basics
```
> This repository is mirrored on [Codeberg](https://codeberg.org/martials/martials.no)
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
│ └── favicon.svg
├── src/
│ ├── components/
│ │ └── Card.astro
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
More to come!

64
TODO.md Normal file
View File

@ -0,0 +1,64 @@
# TODO
- [ ] License
## Code
- [ ] Nix Shell
- [ ] Analytics
- [ ] Organize code better
- [ ] Type slug of project
## SEO
- [ ] Meta tags on each page
## Layout
- [ ] Dark mode toggle
- [x] Navigate using pathname / breadcrumbs
- [ ] Better style for \<code /> blocks
## Accessibility
- [ ] All interactable elements have labels
## I18N
- [ ] Markdown for translations
## ~/
- [ ] About me description
- [ ] Limit latest projects to N (5?)
## ~/about
- [ ] About me
## ~/links
## ~/projects
- [ ] Translate projects
- [ ] RSS Feed
## ~/projects/[project]
- [ ] Only use Gitea icon for Gitea links
- [ ] Bachelor project
- [x] Sparebank1 ActualBudget service
- [ ] More about this website
- [ ] NixOS on desktop
- [ ] Copy link to h tag and scroll to h tag on load
- [x] External links should open in new tab
- [x] Add keywords to meta tag
- [x] Add description to meta tag
- [ ] Source on image if "borrowed" from somewhere
## ~/slashes
- [ ] List of all slashes
## ~/uses
- [x] Homelab uses
- [x] Raspberry PI uses
- [ ] Hardware anchor
## ~/certifications
- [ ] Embed certifications
## ~/tools
### /simplify-truths
- [ ] Merge simplify truths implementation

View File

@ -1,7 +1,7 @@
// @ts-check
import { defineConfig, envField } from "astro/config"
import paraglide from "@inlang/paraglide-astro"
import tailwind from "@astrojs/tailwind"
import tailwindcss from "@tailwindcss/vite"
import sitemap from "@astrojs/sitemap"
import svelte from "@astrojs/svelte"
import node from "@astrojs/node"
@ -10,39 +10,45 @@ import icon from "astro-icon"
import { loadEnv } from "vite"
const { url } = process.env.URL
? loadEnv(process.env.URL, process.cwd(), "")
: { url: "http://localhost:3000" }
const { URL } = process.env.NODE_ENV
? loadEnv(process.env.NODE_ENV, process.cwd(), "")
: { URL: "http://localhost:3000" }
// https://astro.build/config
export default defineConfig({
site: url,
site: URL,
output: "server",
i18n: {
defaultLocale: "nb",
locales: ["nb", "en"]
locales: ["nb", "en"],
},
integrations: [
tailwind(),
sitemap(),
mdx(),
svelte(),
icon(),
paraglide({
// recommended settings
project: "./project.inlang",
outdir: "./src/paraglide" //where your files should be
})
outdir: "./src/paraglide",
}),
],
adapter: node({
mode: "standalone"
mode: "standalone",
}),
vite: {
plugins: [tailwindcss()],
},
markdown: {
shikiConfig: {
theme: "catppuccin-mocha",
},
},
env: {
schema: {
DOMAIN: envField.string({ context: "client", access: "public" }),
URL: envField.string({ context: "client", access: "public" }),
GIT_URL: envField.string({ context: "client", access: "public" }),
STATUS_URL: envField.string({ context: "client", access: "public" })
}
}
})
STATUS_URL: envField.string({ context: "client", access: "public" }),
},
},
})

View File

@ -1,17 +1,20 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"archidektMessage": "My MTG cards and decks",
"hiIm": "Hi, I'm",
"position": "Software Engineer",
"aboutMe": "Some bullshit about me",
"aboutMe": "Dedicated developer currently working at Capgemini Bergen.",
"home": "Home",
"contactMe": "Contact me",
"myLinks": "My links",
"myProjects": "My projects",
"uses": "Uses",
"hardware": "Hardware",
"accessories": "Accessories",
"sourceCode": "Source code",
"createdAt": "Created at",
"updatedAt": "Updated at",
"forMirrors": "For mirrors of Gitea",
"forPersonalProjects": "For personal projects",
"status": "Status",
"name": "Name",

View File

@ -1,17 +1,20 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"archidektMessage": "Mine MTG kort og decks",
"hiIm": "Hei, jeg er",
"position": "Programvareutvikler",
"aboutMe": "Bunntekst",
"aboutMe": "Engasjert utvikler som for tiden jobber hos Capgemini Bergen.",
"home": "Hjem",
"contactMe": "Kontakt meg",
"myLinks": "Mine lenker",
"myProjects": "Mine prosjekter",
"uses": "Uses",
"hardware": "Maskinvare",
"accessories": "Tilbehør",
"sourceCode": "Kildekode",
"createdAt": "Opprettet",
"updatedAt": "Oppdatert",
"forMirrors": "For mirrors av Gitea",
"forPersonalProjects": "For personlige prosjekter",
"status": "Status",
"name": "Navn",

View File

@ -1,41 +1,43 @@
{
"name": "martials-no-v2",
"type": "module",
"version": "0.0.1",
"version": "1.0.0",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide && astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"type-check": "astro check",
"postinstall": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide",
"format": "prettier --write \"./src/**/*.{js,mjs,ts,astro,svelte,css,md,json}\"",
"watch-messages": "paraglide-js compile --watch --project ./project.inlang --outdir ./src/paraglide"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/mdx": "4.0.0-beta.2",
"@astrojs/node": "9.0.0-beta.2",
"@astrojs/mdx": "^4.0.8",
"@astrojs/node": "9.1.1",
"@astrojs/sitemap": "^3.2.1",
"@astrojs/svelte": "6.0.0-beta.1",
"@astrojs/tailwind": "^5.1.2",
"@iconify-json/pajamas": "^1.2.3",
"@inlang/paraglide-astro": "^0.2.2",
"@inlang/paraglide-js": "1.11.2",
"@tailwindcss/typography": "^0.5.15",
"astro": "5.0.0-beta.5",
"astro-icon": "^1.1.1",
"@astrojs/svelte": "^7.0.4",
"@iconify-json/pajamas": "^1.2.5",
"@inlang/paraglide-astro": "^0.3.5",
"@inlang/paraglide-js": "1.11.8",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.0.9",
"astro": "^5.4.1 ",
"astro-icon": "^1.1.5",
"dayjs": "^1.11.13",
"sharp": "^0.33.5",
"svelte": "^4.2.19",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.3"
"svelte": "^5.20.4",
"tailwindcss": "^4.0.9",
"typescript": "^5.7.3"
},
"devDependencies": {
"daisyui": "^4.12.13",
"prettier": "^3.3.3",
"daisyui": "^5.0.0",
"prettier": "^3.5.2",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-svelte": "^3.2.7",
"vite": "^5.4.8"
"prettier-plugin-svelte": "^3.3.3",
"vite": "^6.2.0"
},
"prettier": {
"semi": false,

2807
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/favicon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

View File

@ -0,0 +1,38 @@
---
import { type NavLink, resolvePathname } from "@/utils/linking"
import LocaleLink from "@/components/links/LocaleLink.astro"
const pathname = resolvePathname(Astro.originPathname)
let paths: string[]
if (pathname === "/") {
paths = ["~"]
} else {
paths = ["~", ...pathname.split("/").slice(1)]
}
function getLink(path: string): NavLink {
switch (path) {
case "~":
return "/"
default:
return `/${path}` as NavLink
}
}
---
<div>
{
paths.map((path, index) => (
<span>
{index != paths.length - 1 ? (
<span>
<LocaleLink to={getLink(path)}>{path}</LocaleLink>/
</span>
) : (
path
)}
</span>
))
}
</div>

View File

@ -4,14 +4,28 @@ import * as m from "@/paraglide/messages.js"
// TODO self-host email server
---
<form class="flex flex-col gap-2 max-w-[500px] mx-auto" method="post" action="https://formspree.io/f/mknykgbn">
<Input label={m.name()} type="text" name="name" required />
<Input label={m.subject()} name="subject" required />
<Input label={m.email()} name="_replyto" />
<input name="_gotcha" type="text" class={"hidden"} /> { /*Honeypot spam filter*/}
<label class="flex flex-col">
{m.message()}
<textarea name="message" class="textarea textarea-bordered" required></textarea>
</label>
<button type="submit">{m.send()}</button>
</form>
<div class="max-w-[500px] mx-auto">
<form
class="flex flex-col gap-2 w-full"
method="post"
action="https://formspree.io/f/mknykgbn"
>
<Input label={m.name()} type="text" name="name" required />
<Input label={m.subject()} name="subject" required />
<Input label={m.email()} name="_replyto" />
{/*Honeypot spam filter*/}
<input name="_gotcha" type="text" class={"hidden"} />
<label class="flex flex-col">
{m.message()}
<textarea
name="message"
class="textarea textarea-bordered w-full bg-cat-base"
required></textarea>
</label>
</form>
<button
type="submit"
class="btn mt-2 bg-cat-base border-cat-surface0"
title={m.send()}>{m.send()}</button
>
</div>

View File

@ -9,12 +9,22 @@ import * as m from "@/paraglide/messages"
const giteaLink = `${GIT_URL}/martials/martials.no`
---
<div class="divider" />
<div class="py-5 flex flex-row gap-1 justify-around w-full items-center">
<div class="divider bg-inherit"></div>
<div
class="max-w-[1000px] sm:min-w-[500px] mx-auto py-5 flex flex-row flex-wrap gap-5 justify-around items-center bg-inherit px-5"
>
<div>
<GiteaLink href={giteaLink} />
<ExternalLink href={STATUS_URL} class="flex items-center" title="Status">
<PajamasIcon name="pajamas:status-health" class="w-6 h-6 mr-2" />
<GiteaLink href={giteaLink} class="!text-cat-text" />
<ExternalLink
href={STATUS_URL}
class="flex items-center !text-cat-text"
title="Status"
>
<PajamasIcon
name="pajamas:status-health"
class="w-6 h-6 mr-2"
aria-label="Status health icon"
/>
{m.status()}
</ExternalLink>
</div>

View File

@ -1,34 +0,0 @@
<script lang="ts">
import Select from "./Select.svelte"
import * as m from "@/paraglide/messages"
import CollapseList from "@/components/collapse/CollapseList.svelte"
export let hardware: any[] = []
const hardwareOptions = hardware.map((item) => ({
key: item.id,
value: item.data.title
}))
let selectedHardwareKey: string = hardware[0].id
$: selectedHardware = hardware.find((item) => item.id === selectedHardwareKey)!
// TODO bind to component
function onChange({ detail }: CustomEvent<string>) {
selectedHardwareKey = detail
}
</script>
<div class="px-2 max-w-[750px] sm:min-w-[750px] w-screen">
<h1 class="text-center">{m.hardware()}</h1>
<div>
<Select options={hardwareOptions} on:change={onChange} class="mx-auto w-max" />
</div>
<br />
<CollapseList items={selectedHardware.data.hardware} title={m.hardware()} />
<div class="my-2" />
{#if (selectedHardware.data.accessories)}
<CollapseList items={selectedHardware.data.accessories} title={m.accessories()} />
{/if}
</div>

View File

@ -16,10 +16,10 @@ const {
} = Astro.props
---
<label class="flex flex-col">
<label class="flex flex-col w-full">
{label}
<input
class="input input-bordered"
class="input input-bordered w-full bg-cat-base"
type={type}
name={name}
required={required}

View File

@ -2,10 +2,26 @@
import LocaleLink from "./links/LocaleLink.astro"
import { type NavLink, resolvePathname } from "@/utils/linking"
const currentPath = resolvePathname(Astro.url.pathname)
const pathname = Astro.url.pathname
const currentPath = resolvePathname(pathname)
const isEnglish = pathname.startsWith("/en")
---
<div class="join">
<LocaleLink to={currentPath as NavLink} lang="nb" class="btn join-item">Norsk</LocaleLink>
<LocaleLink to={currentPath as NavLink} lang="en" class="btn join-item">English</LocaleLink>
<LocaleLink
to={currentPath as NavLink}
lang="nb"
class:list={[
"btn join-item !text-cat-text border-cat-surface0",
!isEnglish ? "bg-cat-mantle" : "bg-cat-base",
]}>Norsk</LocaleLink
>
<LocaleLink
to={currentPath as NavLink}
lang="en"
class:list={[
"btn join-item !text-cat-text border-cat-surface0",
isEnglish ? "bg-cat-mantle" : "bg-cat-base",
]}>English</LocaleLink
>
</div>

View File

@ -1,14 +0,0 @@
---
import LocaleLink from "./links/LocaleLink.astro"
import Links from "@/links"
---
<div class="flex justify-end">
{
Links.map(({ to, label }) => (
<LocaleLink to={to} class="m-2 hover:underline">
{label()}
</LocaleLink>
))
}
</div>

View File

@ -1,20 +1,22 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
// TODO move to types?
interface Option {
key: string
interface Option<Key> {
key: Key
value: string
}
export let options: Option[] = []
// TODO bind data instead of dispatching events
const dispatch = createEventDispatcher<{ change: string }>()
interface Props<Key = string> {
selected: Key
options?: Option<Key>[]
class?: string
}
let { selected = $bindable(), options = [], class: clazz }: Props = $props()
</script>
<select
class="select select-bordered w-full max-w-xs ${$$restProps.class}"
on:change={(value) => dispatch("change", value.currentTarget.value)}
bind:value={selected}
class="select select-bordered w-full max-w-xs ${clazz}"
>
{#each options as { key, value }}
<option value={key}>{value}</option>

View File

@ -0,0 +1,30 @@
---
import * as m from "@/paraglide/messages"
import CollapseList from "@/components/collapse/CollapseList.svelte"
import type { CollectionEntry } from "astro:content"
interface Props {
uses: ReadonlyArray<CollectionEntry<"uses">>
}
const { uses } = Astro.props
// TODO set url anchor to selected hardware
---
<div class="px-2 max-w-[750px] sm:min-w-[750px] w-screen">
{
uses.map((hardware) => (
<div class="my-5">
<h3>{hardware.data.title}</h3>
<CollapseList items={hardware.data.hardware} title={m.hardware()} />
<div class="my-2" />
{hardware.data.accessories && (
<CollapseList
items={hardware.data.accessories}
title={m.accessories()}
/>
)}
</div>
))
}
</div>

View File

@ -5,4 +5,4 @@ interface Props {
const { tag } = Astro.props
---
<div class="badge badge-outline">{tag}</div>
<div class="badge badge-outline !text-cat-lavender">{tag}</div>

View File

@ -7,6 +7,6 @@ interface Props {
const { tags } = Astro.props
---
<div class="flex flex-wrap gap-1">
<div class="flex flex-wrap gap-1 py-0.5">
{tags.map((tag) => <Badge tag={tag} />)}
</div>

View File

@ -1,10 +1,17 @@
<script lang="ts">
export let title: string = ""
import type { Snippet } from "svelte"
interface Props {
title?: string
children: Snippet
}
const { title = "", children }: Props = $props()
</script>
<details class="collapse collapse-arrow bg-base-200">
<summary class="collapse-title text-xl font-medium">{title}</summary>
<div class="collapse-content">
<slot />
{@render children()}
</div>
</details>
</details>

View File

@ -1,15 +1,18 @@
<script lang="ts">
import Collapse from "@/components/collapse/Collapse.svelte"
export let items: string[] = []
export let title: string = ""
interface Props {
items?: string[]
title?: string
}
const { items = [], title = "" }: Props = $props()
</script>
<Collapse title={title}>
<Collapse {title}>
<ul>
{#each items as item}
<li class="list-disc ml-5">{item}</li>
{/each}
</ul>
</Collapse>
</Collapse>

View File

@ -0,0 +1,17 @@
---
import PajamasIcon from "@/components/icons/PajamasIcon.astro"
interface Props {
for: string
}
const { for: forId } = Astro.props
---
<label for={forId} aria-label="open sidebar" class="btn btn-square btn-ghost">
<PajamasIcon
name="pajamas:hamburger"
class="w-6 h-6"
aria-label="Hamburger menu"
/>
</label>

View File

@ -0,0 +1,43 @@
---
import Navbar from "./Navbar.astro"
import NavbarDrawer from "./NavbarDrawer.astro"
import HamburgerMenuButton from "./HamburgerMenuButton.astro"
import Breadcrumb from "../Breadcrumb.astro"
import { resolvePathname } from "@/utils/linking"
const currentPath = `~${resolvePathname(Astro.originPathname)}`
const drawerToggleId = "header-drawer"
---
<div class="sm:m-auto">
<div class="drawer drawer-end">
<input id={drawerToggleId} type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col">
<!-- Navbar -->
<div class="navbar w-full justify-end">
<div class="flex justify-between items-center w-full h-full sm:hidden">
<h1 class="!text-xl h-5">
<Breadcrumb />
</h1>
<HamburgerMenuButton for={drawerToggleId} />
</div>
<div class="hidden flex-none sm:block">
<Navbar />
</div>
</div>
</div>
<div class="drawer-side z-50">
<label
for={drawerToggleId}
aria-label="close sidebar"
class="drawer-overlay"></label>
<!-- Drawer -->
<ul class="menu bg-cat-base min-h-full w-80 p-4">
<li class="text-xl font-bold my-5">
{currentPath}
</li>
<NavbarDrawer />
</ul>
</div>
</div>
</div>

View File

@ -0,0 +1,17 @@
---
import LocaleLink from "@/components/links/LocaleLink.astro"
import Links from "@/links"
---
<div class="flex justify-end">
{
Links.map(({ to, label }) => (
<LocaleLink
to={to}
class={"m-2 not-hover:!text-cat-text font-bold font-mono"}
>
~/{label()}
</LocaleLink>
))
}
</div>

View File

@ -0,0 +1,12 @@
---
import Links from "../../links"
import LocaleLink from "../links/LocaleLink.astro"
---
{
Links.map((link) => (
<li>
<LocaleLink to={link.to}>{link.label}</LocaleLink>
</li>
))
}

View File

@ -1,10 +0,0 @@
---
import PajamasIcon from "./PajamasIcon.astro"
interface Props {
class?: string
}
const { class: clazz } = Astro.props
---
<PajamasIcon name="pajamas:gitea" class={clazz}></PajamasIcon>

View File

@ -1,13 +1,13 @@
---
import { Image } from "astro:assets"
import me from "@/images/polite-cat.jpg"
import me from "@/images/me.jpg"
import * as m from "@/paraglide/messages.js"
import "@/styles/global.css"
---
<div class="flex items-center justify-around flex-wrap">
<div class="m-5">
<h1 class="sm:text-7xl font-bold">
<h1>
{m.hiIm()}
<br />
Martin Berg Alstad
@ -16,7 +16,14 @@ import "@/styles/global.css"
</h1>
<p class="mx-1 sm:my-10">{m.aboutMe()}</p>
</div>
<Image src={me} alt="Me on a hike" width="400" height="400" class="p-5 mx-auto" loading={"eager"} />
<Image
src={me}
alt="Me on a stand in front of a poster that says 'anbudsassistent'"
width="400"
height="400"
class="p-5 mx-auto rounded-full"
loading={"eager"}
/>
</div>
<!-- Mastodon verification -->
<a rel="me" href="https://snabelen.no/@Martials" class="hidden">Mastodon</a>
<a rel="me" href="https://snabelen.no/@Martials" class="hidden">Mastodon</a>

View File

@ -0,0 +1,7 @@
---
import Greeting from "./Greeting.astro"
import LatestProjects from "./LatestProjects.astro"
---
<Greeting />
<LatestProjects />

View File

@ -0,0 +1,10 @@
---
import { getCollection } from "astro:content"
import ProjectGrid from "@/components/projects/ProjectGrid.astro"
const projects = await getCollection("projects")
---
<div class="divider"></div>
<ProjectGrid projects={projects} />

View File

@ -7,6 +7,12 @@ interface Props extends LinkProps {
const { href, noStyle = false, class: clazz, ...props } = Astro.props
---
<a href={href} target="_blank" rel="noopener" class:list={[noStyle ? "" : "link", clazz]} {...props}>
<a
href={href}
target="_blank"
rel="noopener"
class:list={[noStyle ? "" : "link", clazz]}
{...props}
>
<slot />
</a>

View File

@ -1,16 +1,21 @@
---
import ExternalLink from "./ExternalLink.astro"
import * as m from "@/paraglide/messages"
import Gitea from "../icons/Gitea.astro"
import PajamasIcon from "../icons/PajamasIcon.astro"
interface Props {
href: string
class?: string
}
const { href } = Astro.props
const { href, class: clazz } = Astro.props
---
<div>
<ExternalLink href={href} class="flex items-center gap-1">
<Gitea class="w-6 h-6" />
<ExternalLink
href={href}
class:list={["flex items-center gap-1", clazz]}
title="Gitea"
>
<PajamasIcon name="pajamas:gitea" class="w-6 h-6" aria-label="Gitea icon" />
{m.sourceCode()}
</ExternalLink>
</div>

View File

@ -8,12 +8,27 @@ interface Props extends MyLink {
}
const { title, message, url, icon, class: clazz } = Astro.props
const iconStyle = "w-6 h-6"
---
<ExternalLink href={url} noStyle>
<div class:list={["card bg-neutral", clazz]}>
<div class:list={["card bg-cat-mantle text-cat-text", clazz]}>
<div class="card-body p-5 flex flex-row items-center">
<PajamasIcon name={icon ?? "pajamas:link"} class="w-6 h-6" />
{
icon ? (
<PajamasIcon
name={icon.src}
class={iconStyle}
aria-label={icon.alt}
/>
) : (
<PajamasIcon
name={"pajamas:link"}
class={iconStyle}
aria-label="Link icon"
/>
)
}
<div>
<h5 class="card-title">{title}</h5>
<p class="prose">{message}</p>

View File

@ -2,10 +2,8 @@
import links from "./myLinks"
import LinkCard from "./LinkCard.astro"
import "@/styles/global.css"
import * as m from "@/paraglide/messages"
---
<h1 class="text-center">{m.myLinks()}</h1>
<div class="flex flex-col mx-auto w-fit gap-5">
{
links.map((link) => (

View File

@ -3,44 +3,81 @@ import { GIT_URL } from "astro:env/client"
import * as m from "@/paraglide/messages"
export interface MyLink {
title: string;
url: string;
message?: string;
icon?: PajamasIcon
title: string
url: string
message?: string
icon?: {
src: PajamasIcon
alt: string
}
}
export default [
{
title: "GitHub",
url: "https://github.com/emberal",
icon: "pajamas:github"
title: "Archidekt",
url: "https://archidekt.com/u/Emberal",
message: m.archidektMessage(),
},
{
title: "Bluesky",
url: "https://bsky.app/profile/martials.no",
},
{
title: "Codeberg",
url: "https://codeberg.org/martials",
message: m.forMirrors(),
icon: {
src: "pajamas:git",
alt: "Git icon",
},
},
{
title: "Gitea",
url: `${GIT_URL}/martials`,
message: m.forPersonalProjects(),
icon: "pajamas:gitea"
icon: {
src: "pajamas:gitea",
alt: "Gitea icon",
},
},
{
title: "GitHub",
url: "https://github.com/emberal",
icon: {
src: "pajamas:github",
alt: "GitHub icon",
},
},
{
title: "LinkedIn",
url: "https://www.linkedin.com/in/martin-b-2a69391a3/",
icon: "pajamas:linkedin"
icon: {
src: "pajamas:linkedin",
alt: "LinkedIn icon",
},
},
{
title: "Mastodon (Snabelen)",
title: "Mastodon (Snabelen.no)",
url: "https://snabelen.no/@Martials",
icon: "pajamas:mastodon"
icon: {
src: "pajamas:mastodon",
alt: "Mastodon icon",
},
},
{
title: "ListenBrainz",
url: "https://listenbrainz.org/user/emberal/",
},
{
title: "Pixelfed",
url: "https://pixelfed.social/i/web/profile/261454857934868480"
url: "https://pixelfed.social/i/web/profile/261454857934868480",
},
{
title: "Steam",
url: "https://steamcommunity.com/id/martials/"
url: "https://steamcommunity.com/id/martials/",
},
{
title: "Trakt.tv",
url: "https://trakt.tv/users/martials"
}
url: "https://trakt.tv/users/martials",
},
] satisfies MyLink[]

View File

@ -1,37 +1,8 @@
---
import ProjectCard from "./ProjectCard.astro"
import * as m from "@/paraglide/messages"
import { type CollectionEntry } from "astro:content"
import { type NavLink } from "@/utils/linking"
import { getCollection } from "astro:content"
import ProjectGrid from "./ProjectGrid.astro"
interface Props {
projects: CollectionEntry<"projects">[]
}
const { projects } = Astro.props
const baseUrl: NavLink = "/projects"
const projects = await getCollection("projects")
---
<h1 class="text-4xl font-bold text-center sm:my-10 mt-2">{m.myProjects()}</h1>
<div class="flex flex-wrap justify-around">
{
projects.map(
({
data: { title, description, tags, heroImage, heroImageAlt },
id
}) => (
<div class="my-5 px-2">
<ProjectCard
title={title}
linkTo={`${baseUrl}/${id}`}
description={description}
tags={tags}
image={heroImage}
imageAlt={heroImageAlt}
/>
</div>
)
)
}
</div>
<ProjectGrid projects={projects} />

View File

@ -19,12 +19,13 @@ const { title, description, tags, image, imageAlt, linkTo } = Astro.props
<LocaleLink
to={linkTo}
class="card bg-base-100 max-w-96 shadow-xl hover:scale-105 transition"
class="card bg-cat-base max-w-96 shadow-xl hover:scale-105 transition border border-cat-surface0"
>
<figure>
<Image src={image} alt={imageAlt} />
<figcaption class="sr-only">{imageAlt}</figcaption>
</figure>
<div class="card-body">
<div class="card-body text-cat-text">
<h2 class="card-title">
{title}
</h2>

View File

@ -0,0 +1,40 @@
---
import type { Project } from "@/types/types"
import type { NavLink } from "@/utils/linking"
import ProjectCard from "./ProjectCard.astro"
import dayjs from "dayjs"
interface Props {
projects: ReadonlyArray<Project>
}
const { projects } = Astro.props
const baseUrl: NavLink = "/projects"
---
<div class="flex flex-wrap justify-around">
{
projects
.toSorted((a, b) =>
dayjs(a.data.updatedAt).isBefore(dayjs(b.data.updatedAt)) ? 1 : -1,
)
.map(
({
data: { title, description, tags, heroImage, heroImageAlt },
id,
}) => (
<div class="my-5 px-2">
<ProjectCard
title={title}
linkTo={`${baseUrl}/${id}`}
description={description}
tags={tags}
image={heroImage}
imageAlt={heroImageAlt}
/>
</div>
),
)
}
</div>

View File

@ -1,11 +1,12 @@
---
import Layout from "@/layouts/Layout.astro"
import { Image } from "astro:assets"
import { getEntry, render } from "astro:content"
import BadgeList from "../badge/BadgeList.astro"
import * as m from "@/paraglide/messages"
import Layout from "@/layouts/Layout.astro"
import BadgeList from "@/components/badge/BadgeList.astro"
import GiteaLink from "@/components/links/GiteaLink.astro"
import { languageTag } from "@/paraglide/runtime"
import GiteaLink from "../links/GiteaLink.astro"
import { getEntry, render } from "astro:content"
import { Image } from "astro:assets"
import dayjs from "dayjs"
import "@/styles/global.css"
interface Props {
@ -15,39 +16,55 @@ interface Props {
const { project } = Astro.props
const entry = await getEntry("projects", project)
const { Content } = await render(entry)
const { Content } = await render(entry!)
const {
lang,
title,
description,
tags,
keywords,
heroImage,
heroImageAlt,
source,
createdAt,
updatedAt
updatedAt,
} = entry!.data
function localeDateString(isoString: string): string {
let template = "DD-MM-YYYY"
if (languageTag() === "nb") {
template = "DD/MM/YYYY"
}
return dayjs(isoString).locale(languageTag()).format(template)
}
---
<!--TODO day.js for dates?-->
<Layout title={title} class="mx-auto max-w-[750px]">
<Layout
title={title}
class="mx-auto max-w-[750px]"
description={description}
keywords={keywords}
>
<div class="flex justify-between my-2">
<div>
<h1>{title}</h1>
<h2>{title}</h2>
<BadgeList tags={tags} />
</div>
<div class="flex flex-col items-end">
<p>
{m.createdAt()}: {new Date(createdAt).toLocaleDateString(languageTag())}
{m.createdAt()}: {localeDateString(createdAt)}
</p>
<p>
{m.updatedAt()}: {new Date(updatedAt).toLocaleDateString(languageTag())}
{m.updatedAt()}: {localeDateString(updatedAt)}
</p>
</div>
</div>
<Image src={heroImage} alt={heroImageAlt} />
<Image src={heroImage} alt={heroImageAlt} class="m-auto" />
<GiteaLink href={source} />
<GiteaLink href={source} class="my-2" />
<p class="my-2">{description}</p>
<Content />
<div lang={lang}>
<Content />
</div>
</Layout>

View File

@ -5,27 +5,29 @@ const projectCollection = defineCollection({
loader: glob({ pattern: "**\/*.mdx", base: "./src/content/projects" }),
schema: ({ image }) =>
z.object({
lang: z.union([z.literal("en"), z.literal("nb")]),
title: z.string(),
description: z.string(),
heroImage: image(),
heroImageAlt: z.string(),
tags: z.array(z.string()),
source: z.string(),
createdAt: z.string(),
updatedAt: z.string()
})
keywords: z.array(z.string()),
source: z.string().url(),
createdAt: z.string().date(),
updatedAt: z.string().date(),
}),
})
const hardwareCollection = defineCollection({
loader: glob({ pattern: "**\/*.yaml", base: "./src/content/hardware" }),
const usesCollection = defineCollection({
loader: glob({ pattern: "**\/*.yaml", base: "./src/content/uses" }),
schema: z.object({
title: z.string(),
accessories: z.optional(z.array(z.string())),
hardware: z.array(z.string())
})
hardware: z.array(z.string()),
}),
})
export const collections = {
projects: projectCollection,
hardware: hardwareCollection
uses: usesCollection,
}

View File

@ -1,3 +0,0 @@
title: Home Server
hardware:
- b # Graphics cards, CPUs, etc.

View File

@ -1,5 +0,0 @@
title: Raspberry Pi 4
accessories:
- a # Screens, keyboards, mice, etc.
hardware:
- b # Graphics cards, CPUs, etc.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -1,14 +1,19 @@
---
title: "Hjemmeside"
description: "Hjemmesiden"
heroImage: "./kevin-james.jpg"
heroImageAlt: "The homepage of this site"
tags: [Astro, Svelte, TypeScript, I18n]
source: "https://example.com"
lang: "en"
title: "Welcome"
description: "Welcome to my homepage / portfolio"
heroImage: "assets/recursive-meme.png"
heroImageAlt: "A recursive meme that says: Self-reference, recursive meme is self-referential"
tags: [Astro, Svelte, TypeScript, I18n, TailwindCSS, Docker]
keywords: [Martin Berg Alstad, portfolio, homepage, website, martials, emberal]
source: "https://git.martials.no/martials/martials.no"
createdAt: "2024-09-22"
updatedAt: "2024-09-22"
updatedAt: "2025-02-15"
---
This is a short meta post about the homepage of this site.
It is a simple landing page with a short introduction to the site and a list of the latest posts.
The site is not built with GatsbyJS and Contentful.
This is a short meta post about my homepage which you are looking at right now.
This page is going to be a simple site where I will share my projects, stuff i've accomplished and things about me.
It is still a work in progress, so it will be updated for the forseable future.
In the meantime, you can see what i'm working on at [Gitea](https://git.martials.no/martials?tab=activity).

View File

@ -1,16 +0,0 @@
---
title: "API for å forenkle TimeEdit iCalendar filer"
description: "Et API som forenkler tittler i ICS filer fra TimeEdit"
heroImage: "./Calendar before and after.png"
heroImageAlt: "En kalender før og etter APIet har blitt brukt. Venstre side er før, høyre etter."
tags: [API, Kotlin, Spring Boot, Tomcat, iCal4j, CI/CD]
source: "https://github.com/emberal/hvl_ics_simplifier"
createdAt: "2023-08-08"
updatedAt: "2024-10-20"
---
Ble lei av hvor vanskelig det var å lese iCalendar filer fra HVL sin kalender i TimeEdit, så jeg lagde et API som gjør det enklere.
Data for tittelen blir hentet fra den gamle tittelen.
For å finne hvilken type hendselse det er, sjekkes beskrivelsen og lokasjonen.
APIet er skrevet i Kotlin med Spring boot, og hosted på en selvhostet tomcat server.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

View File

@ -0,0 +1,115 @@
---
lang: "en"
title: "Sparebank1 - Actual Budget"
description: "Automatically import transactions from Sparebank1 bank accounts to Actual Budget."
heroImage: "assets/is_it_worth_the_time.png"
heroImageAlt: "A diagram that shows how much time is saved automating a tasks rather than doing it manually."
tags: [TypeScript, Docker, Node, CronJob, Sqlite, API-management]
keywords: [Finance, Sparebank1, Sparebank1 Utvikling, Sparebank1 API, Actual Budget API]
source: "https://git.martials.no/martials/sparebank1_actual_budget_integration"
createdAt: "2025-02-27"
updatedAt: "2025-02-27"
---
import ExternalLink from "@/components/links/ExternalLink.astro"
## What is it?
<ExternalLink href="https://actualbudget.org/">Actual Budget</ExternalLink> is an open-source budgeting platform, it can very simply be self-hosted and accessed throught the browser.
While <ExternalLink href="https://www.sparebank1.no/nb/bank/privat.html">Sparebank1</ExternalLink> is a Norwegian bank, with a great <ExternalLink href="https://developer.sparebank1.no/#/">developer experience</ExternalLink> and an easy to use API that can be used to fetch transactions for example.
Actual has an API that can be used to interact with your own instance of Actual, like importing transactions.
Which is what the Sparebank1 Actual Budget integration does. It fetches transactions from my accounts in Sparebank1 and imports them automatically into Actual.
The operation runs daily and can be configured to import from multiple accounts at once.
The purpose of this application is to automatically transfer transactions to the budget, to avoid doing it manually often, or once in a while.
<br/>
## The techy stuff
Since the <ExternalLink href="https://actualbudget.org/docs/api/">Actual Budget API</ExternalLink> is an NPM package, the application had to be created in TypeScript.
I looked at options to NodeJS but neither Bun or Deno was compatible with the *better-sqlite3* package which was a dependency to the API,
so the best option for now was NodeJS.
The application runs a cronJob that executes once a day at 1 am. The specific time is not really that important since it fetches transactions from 3-4 days before the day it runs.
This is done to avoid fetching transactions which are not fully cleared yet. Some transactions can even be cleared but the unique Id for the transaction might not be set yet.
Actual uses that unique Id in order to avoid duplicate transactions, and makes it possible to update transactions if the content has changed.
Rules can be defined within the Actual application, so transactions are automatically sorted into the correct categories on import.
The application supports importing multiple accounts from a single user.
This is done by specifying the account keys to Sparebank1 and a equal length array of ids for the Actual accounts.
<br/>
### Low Coupling
In order to make it easy to reuse the same application for other banks, it was created with low coupling in mind.
Interfaces are defined for the different parts of the application, and in order to implement it for any other bank,
the `Bank` interface can be implemented on any class, and requires two methods.
<br/>
```ts
export interface Bank {
fetchTransactions: (
interval: Interval,
...accountKeys: ReadonlyArray<string>
) => Promise<ReadonlyArray<ActualTransaction>>
shutdown: () => Promise<void> | void
}
```
<br/>
### Authentication
Both Sparebank1 and Actual requires different secrets and keys to work.
Actual only requires that secrets are passed into the client that handles the requests, but Sparebank1 requires more effort in order to fetch data.
First it requires a *authentication code* that can only be fetched using my own BankID account.
The authentication code must be swapped for a *refresh token* within 2 minutes or the process must be started over.
The refresh token has a lifetime of a year unless used, so it can be saved for later use.
The refresh token is added as an environmental variable that can be used to fetch the first *access token*.
After that, both the new refresh token and access token is stored in a Sqlite database so they can be reused until they expire.
<br/>
### Deployement
The application runs in a docker container on my [Homelab](/uses).
It is build from a Gitea Act runner that adds the needed secrets and variables from the Gitea instance.
<br/>
```yaml
name: Deploy application
on:
push:
branches: [main]
jobs:
deploy:
runs-on: host
env:
# Secrets and vars
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Run docker-compose
run: docker compose up -d --build
```
<br/>
## The road ahead
Future plans incude adding more features to the application and some more error handling.
Then potentially implementing the integration for other banks or credit cards to automate even more.
I also want to rewrite it in Bun when they implement the needed APIs to get Better-sqlite3 working.

View File

@ -1,26 +0,0 @@
---
title: "Forenkle sannhetsverdier og sannhetstabeller"
description: "Forenkle sannhetsuttrykk og opprette sannhetstabeller"
heroImage: "./Simplify-truths-website.png"
heroImageAlt: "Nettsiden med en sannhetstabell"
tags: [TypeScript, SolidJS, Tailwind css, Nettside, Java, API, Spring Boot, Raspberry Pi, Apache, Tomcat]
source: "https://github.com/h600878/martials.no"
createdAt: "2022-11-08"
updatedAt: "2024-10-20"
---
{/* TODO change to Rust */}
Noen sannhetsverdier kan bli ganske store, og kompliserte. Derfor har jeg laget dette programmet for å forenkle uttrykk mest mulig.
Programmet bruker flere kjente metoder for å skrive om uttrykkene. Hvilke uttrykk som er brukt og hvilke endringer de gjorde,
kan vises i menyen under søkefeltet.
<br />
I tillegg til å bare forenkle uttrykk, kan man også generere en sannhetstabell med alle mulige verdier i uttrykket.
Hvis man ikke ønsker å forenkle uttrykket, men bare generere tabellen, kan man enkelt skru av forenkling. I tillegg har
man muligheten til å filtrere resultat, enten ved å skjule sanne eller usanne verdier. Eller ved å sortere etter sanne
eller usanne først.
<br />
Nettsiden er laget med TypeScript, SolidJS og Tailwind CSS. API-et er laget med Java og Spring Boot.
Begge kjører på min egen raspberry pi 4, nettsiden er hostet på en apache2 tjener, mens API-et ligger på en tomcat tjener.
<a href={"https://martials.no/simplify-truths.html"}>Lenke til nettsiden.</a>
<br />
Spørringer kan gjøres mot API-et kan utføres med GET eller POST. API-et returnerer JSON.
[Lenke til API-docs](https://api.martials.no/simplify-truths)

View File

@ -2,8 +2,8 @@ title: Desktop
accessories:
- Gaming chair | Arozzi Mezzo V2 Gaming chair Fabric Black/Red # https://www.komplett.no/product/1079732?noredirect=true
- Headset | Logitech PRO X LIGHTSPEED Wireless Gaming Headset # https://www.komplett.no/product/1162749?noredirect=true
- Keyboard | Logitech G710
- Monitor 1 | AOC 27" LED FreeSync G2790PX # https://www.komplett.no/product/975642?noredirect=true
- Keyboard | Keychron K8 Pro QMK/VIA RGB Gateron Red # https://www.komplett.no/product/1303473/gaming/gaming-utstyr/gamingtastatur/keychron-k8-pro-qmkvia-rgb-gateron-red-traadloest-gamingtastatur-sort
- Monitor 1 | Philips 34" 34M2C6500/00 # https://www.komplett.no/product/1307753/gaming/gaming-utstyr/gamingskjermer/philips-34-gamingskjerm-34m2c650000
- Monitor 2 | Asus 28" 4K LED PB287Q # https://www.komplett.no/product/815114?noredirect=true
- Mouse | Logitech G502 HERO Gaming Mouse
- Mousepad | Svive Styx ESGR Gaming Mousepad XXL # https://www.komplett.no/product/985884?noredirect=true

View File

@ -0,0 +1,3 @@
title: Homelab
hardware:
- HP ProDesk 600 G3 SFF i7 6. gen # https://bergenbruktpc.no/stasjonaer-pc/hp/hp-prodesk-600-g3-sff-i5-i7-6-gen#&variation=926583

View File

@ -0,0 +1,5 @@
title: Raspberry Pi 4
accessories:
- 4 TB External harddrive
hardware:
- OKdo Raspberry Pi 4 Kit 4 GB # https://www.kjell.com/no/produkter/data/raspberry-pi/okdo-raspberry-pi-4-kit-4-gb-p88059

BIN
src/images/me.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

View File

@ -1,31 +1,44 @@
---
import Navbar from "@/components/Navbar.astro"
import Footer from "@/components/Footer.astro"
import Header from "@/components/header/Header.astro"
import Breadcrumb from "@/components/Breadcrumb.astro"
import { languageTag } from "@/paraglide/runtime"
interface Props {
title: string
description?: string
keywords?: ReadonlyArray<string>
class?: string
}
const { title, class: clazz } = Astro.props
const { title, description, keywords, class: clazz } = Astro.props
const mainClass =
"grow max-w-[1000px] m-auto sm:min-w-[500px] not-sm:w-full px-5"
---
<!doctype html>
<html lang={languageTag()} dir={"ltr"}>
<head>
<meta charset="UTF-8" />
<meta name="description" content="Astro description" />
<meta name="viewport" content="width=device-width" />
<link rel="sitemap" href="/sitemap-index.xml" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
</head>
<body class="flex flex-col h-screen">
<Navbar />
<main class:list={["grow", clazz]}>
<slot />
</main>
<Footer />
</body>
<head>
<meta charset="UTF-8" />
<meta name="author" content="Martin Berg Alstad" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
{description && <meta name="description" content={description} />}
{keywords && <meta name="keywords" content={keywords.join(", ")} />}
<link rel="sitemap" href="/sitemap-index.xml" />
<link rel="icon" type="image/jpg" href="/favicon.jpg" />
<title>{title} | Martin Berg Alstad</title>
</head>
<body class="flex flex-col min-h-screen bg-cat-base text-cat-text">
<Header />
<main class:list={[mainClass, clazz]}>
<h1 class="text-center not-sm:hidden">
<Breadcrumb />
</h1>
<div class="my-5">
<slot />
</div>
</main>
<Footer />
</body>
</html>

View File

@ -9,24 +9,20 @@ interface Link {
const Links: Link[] = [
{
label: m.home,
to: "/"
to: "/",
},
{
label: m.myProjects,
to: "/projects"
to: "/projects",
},
{
label: m.myLinks,
to: "/links"
},
{
label: m.hardware,
to: "/hardware"
to: "/links",
},
{
label: m.contactMe,
to: "/contact"
}
to: "/contact",
},
]
export default Links

View File

@ -1,5 +1,6 @@
---
import Layout from "../layouts/Layout.astro"
import "@/styles/global.css"
---
<Layout title="404">

View File

@ -1,8 +1,9 @@
---
import OnePager from "../../components/Greeting.astro"
import Layout from "../../layouts/Layout.astro"
import IndexPage from "@/components/landing/IndexPage.astro"
import Layout from "@/layouts/Layout.astro"
import "@/styles/global.css"
---
<Layout title="Welcome">
<OnePager />
<IndexPage />
</Layout>

View File

@ -1,7 +1,9 @@
---
import Layout from "@/layouts/Layout.astro"
import LinksPage from "@/components/myLinks/LinksPage.astro"
import "@/styles/global.css"
---
<Layout title="My links">
<LinksPage />
</Layout>
</Layout>

View File

@ -1,15 +1,15 @@
---
import ProjectPage from "@/components/projects/ProjectPage.astro"
import { type GetStaticPathsResult } from "astro"
import "@/styles/global.css"
// Prerender the page as static HTML during build
export const prerender = true
export function getStaticPaths(): GetStaticPathsResult {
return [
{ params: { project: "hvl-ics-simplifier" } },
{ params: { project: "homepage" } },
{ params: { project: "simplify-truths" } }
{ params: { project: "sb1budget" } },
]
}

View File

@ -1,11 +1,9 @@
---
import { getCollection } from "astro:content"
import Layout from "@/layouts/Layout.astro"
import MyProjectsPage from "@/components/projects/MyProjectsPage.astro"
const projects = await getCollection("projects")
import "@/styles/global.css"
---
<Layout title="Projects">
<MyProjectsPage projects={projects} />
</Layout>
<MyProjectsPage />
</Layout>

View File

@ -1,12 +1,12 @@
---
import Layout from "@/layouts/Layout.astro"
import HardwarePage from "@/components/HardwarePage.svelte"
import UsesPage from "@/components/UsesPage.astro"
import "@/styles/global.css"
import { getCollection } from "astro:content"
const hardware = await getCollection("hardware")
const uses = await getCollection("uses")
---
<Layout title="Hardware" class="mx-auto max-w-[750px]">
<HardwarePage server:defer hardware={hardware} />
<UsesPage uses={uses} />
</Layout>

View File

@ -1,8 +1,9 @@
---
import Layout from "../layouts/Layout.astro"
import Greeting from "../components/Greeting.astro"
import IndexPage from "@/components/landing/IndexPage.astro"
import Layout from "@/layouts/Layout.astro"
import "@/styles/global.css"
---
<Layout title="Velkommen">
<Greeting />
<IndexPage />
</Layout>

View File

@ -1,7 +1,9 @@
---
import Layout from "@/layouts/Layout.astro"
import LinksPage from "@/components/myLinks/LinksPage.astro"
import "@/styles/global.css"
---
<Layout title="Mine lenker">
<LinksPage />
</Layout>
</Layout>

View File

@ -1,15 +1,15 @@
---
import ProjectPage from "@/components/projects/ProjectPage.astro"
import { type GetStaticPathsResult } from "astro"
import "@/styles/global.css"
// Prerender the page as static HTML during build
export const prerender = true
export function getStaticPaths(): GetStaticPathsResult {
return [
{ params: { project: "hvl-ics-simplifier" } },
{ params: { project: "homepage" } },
{ params: { project: "simplify-truths" } }
{ params: { project: "sb1budget" } },
]
}

View File

@ -1,11 +1,9 @@
---
import { getCollection } from "astro:content"
import Layout from "@/layouts/Layout.astro"
import MyProjectsPage from "@/components/projects/MyProjectsPage.astro"
const projects = await getCollection("projects")
import "@/styles/global.css"
---
<Layout title="Prosjekter">
<MyProjectsPage projects={projects} />
<MyProjectsPage />
</Layout>

View File

@ -1,12 +1,12 @@
---
import Layout from "@/layouts/Layout.astro"
import HardwarePage from "@/components/HardwarePage.svelte"
import UsesPage from "@/components/UsesPage.astro"
import "@/styles/global.css"
import { getCollection } from "astro:content"
const hardware = await getCollection("hardware")
const uses = await getCollection("uses")
---
<Layout title="Hardware" class="mx-auto max-w-[750px]">
<HardwarePage server:defer hardware={hardware} />
<UsesPage uses={uses} />
</Layout>

View File

@ -1,6 +1,28 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "daisyui";
@theme {
--color-cat-rosewater: #f5e0dc;
--color-cat-flamingo: #f2cdcd;
--color-cat-pink: #f5c2e7;
--color-cat-mauve: #cba6f7;
--color-cat-red: #f38ba8;
--color-cat-maroon: #eba0ac;
--color-cat-peach: #fab387;
--color-cat-yellow: #f9e2af;
--color-cat-green: #a6e3a1;
--color-cat-teal: #94e2d5;
--color-cat-sky: #89dceb;
--color-cat-sapphire: #74c7ec;
--color-cat-blue: #89b4fa;
--color-cat-lavender: #b4befe;
--color-cat-text: #cdd6f4;
--color-cat-surface0: #313244;
--color-cat-base: #1e1e2e;
--color-cat-mantle: #181825;
}
@layer utilities {
.debug {
@ -8,21 +30,24 @@
}
}
@layer base {
br {
@apply my-0.5;
}
h1 {
@apply text-4xl font-bold mb-2;
}
h2 {
@apply text-3xl font-bold mb-2;
}
/* TODO change default style*/
a {
@apply link
}
br {
@apply my-0.5;
}
h1 {
@apply text-4xl font-bold mb-2;
}
h2 {
@apply text-3xl font-bold mb-2;
}
h3 {
@apply text-2xl font-bold mb-2;
}
/* TODO change default style*/
a {
@apply link text-cat-mauve;
text-decoration-line: none;
}

View File

@ -1,3 +1,14 @@
export type Icon = "gitea" | "github" | "mastodon" | "linkedin" | "link" | "status-health"
/**
* @see https://icon-sets.iconify.design/pajamas/
*/
export type Icon =
| "git"
| "gitea"
| "github"
| "mastodon"
| "linkedin"
| "link"
| "status-health"
| "hamburger"
export type PajamasIcon = `pajamas:${Icon}`

View File

@ -1,8 +1,8 @@
export interface ComponentProps {
class?: string;
title?: string;
class?: string
title?: string
}
export interface LinkProps extends ComponentProps {
href: string;
href: string
}

View File

@ -7,21 +7,23 @@ interface TranslatedPathnames {
}
export type NavLink =
"/"
| "/"
| "/contact"
| "/projects"
| `/projects/${Project["id"]}`
| "/links"
| "/hardware"
| "/uses"
const paths: Set<NavLink> = new Set([
"/",
"/contact",
"/projects",
"/links",
"/hardware"
"/uses",
])
const projectPaths: Set<string> = new Set<string>(["homepage", "sb1budget"])
/**
* Defines the localized pathnames for the site.
* The key must be used to navigate to the correct path.
@ -33,13 +35,13 @@ const pathnames: Record<AbsolutePathname, TranslatedPathnames> = {}
for (const path of paths) {
pathnames[path] = {
nb: path,
en: `/en${path}`
en: `/en${path}`,
}
}
export function localizePathname(
pathname: NavLink,
locale: AvailableLanguageTag
locale: AvailableLanguageTag,
): string {
const pathnameParts = pathname.split("/")
const firstSegment: AbsolutePathname = `/${pathnameParts[1]}`
@ -63,3 +65,22 @@ export function resolvePathname(pathname: string): AbsolutePathname {
}
return pathname as AbsolutePathname
}
export function isAbsolutePathname(path: string): path is AbsolutePathname {
return path.startsWith("/")
}
export function isNavLink(path: string): path is NavLink {
if (path.startsWith("/en")) {
path = path.slice(2)
}
if (paths.has(path as NavLink)) {
return true
}
const pathSplit = path.split("/").slice(1)
return (
pathSplit.length === 2 &&
pathSplit[0] === "projects" &&
projectPaths.has(pathSplit[1])
)
}

View File

@ -1,8 +0,0 @@
/** @type {import("tailwindcss").Config} */
export default {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
extend: {}
},
plugins: [require("@tailwindcss/typography"), require("daisyui")]
}