25 Commits

Author SHA1 Message Date
25c565dfb3 Fixed language not switching
All checks were successful
Build and deploy website / build (push) Successful in 1m6s
2025-07-10 21:59:16 +02:00
c1714e9654 Update TODO
All checks were successful
Build and deploy website / build (push) Successful in 57s
Signed-off-by: Martin Berg Alstad <git@martials.no>
2025-07-03 19:10:56 +02:00
409c343a1b 👷 Merge into single docker compose file 2025-07-03 19:10:56 +02:00
fa58967164 👷 Update ref keys and master to develop on develop step 2025-07-03 19:10:56 +02:00
e0908c57a8 👷 Staging from develop branch 2025-07-03 19:10:56 +02:00
7030f70b50 Start updating paraglide to v2 2025-07-03 19:10:56 +02:00
5e7100c1e2 🔥 Remove prettier 2025-07-03 19:10:56 +02:00
82d1eea4f7 Update git domain 2025-07-03 19:10:56 +02:00
8d5c61cbaa 🎨 Fix lint errors using Biome 2025-07-03 19:10:55 +02:00
45226136f3 🎨 Format using Biome 2025-07-01 19:45:02 +02:00
a859439353 Adde Biome formatter and linter 2025-07-01 19:43:59 +02:00
969660abc8 ❄️ Add Nix shell 2025-07-01 19:20:26 +02:00
99aced7367 📦 Update dependencies 2025-07-01 19:09:53 +02:00
78b333e9f7 ✍️ Update TODO.md 2025-07-01 19:09:24 +02:00
1d04befff1 🐛 Fix trailing slash 2025-07-01 19:08:34 +02:00
dc4d564059 Breadcrumbs with navigation for mobile
All checks were successful
Build and deploy website / build (push) Successful in 33s
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
43 changed files with 2042 additions and 1952 deletions

2
.env
View File

@ -1,3 +1,3 @@
DOMAIN="martials.no" DOMAIN="martials.no"
GIT_URL=https://git.$DOMAIN GIT_URL=https://code.$DOMAIN
STATUS_URL="https://status.$DOMAIN/status/home" STATUS_URL="https://status.$DOMAIN/status/home"

View File

@ -4,9 +4,7 @@ on:
push: push:
branches: branches:
- master - master
pull_request: - develop
branches:
- master
jobs: jobs:
build: build:
@ -15,5 +13,9 @@ jobs:
steps: steps:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Run docker-compose - name: Build production
run: docker compose up -d --build run: docker compose up -d --build prod
if: gitea.ref == 'refs/heads/master'
- name: Build develop
run: docker compose up -d --build dev
if: gitea.ref == 'refs/heads/develop'

View File

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

60
TODO.md
View File

@ -1,46 +1,64 @@
# TODO # TODO
## Code - [ ] License
- [ ] day.js for dates
- [ ] Nix Shell
## SSE ## Code
- [x] Correct Sitemap.xml - [x] Nix Shell
- [x] Correct robots.txt - [ ] Analytics
- [x] Correct security.txt - [ ] Organize code better
- [ ] Type slug of project
## CI/CD
- [x] Staging environment
- [x] Deploy to staging environment on push to develop
- [x] Deploy to production environment on push to master
- [ ] Staging environment .env file
## SEO
- [ ] Meta tags on each page
## Layout ## Layout
- [x] Show current page
- [x] Correct bg colour on entire page
- [x] Hamburger menu on mobile
- [ ] Dark mode toggle - [ ] Dark mode toggle
- [ ] Navigate using pathname / breadcrumbs - [x] Navigate using pathname / breadcrumbs
- [ ] Better style for \<code /> blocks
## Accessibility ## Accessibility
- [x] Fix colours on buttons
- [x] Correct contrast
- [ ] All interactable elements have labels - [ ] All interactable elements have labels
- [x] Colour links, also in MDX posts
## I18N
- [ ] Markdown for translations
## ~/ ## ~/
- [ ] About me description - [ ] About me description
- [x] Latest projects - [ ] Limit latest projects to N (5?)
- [x] Non-cat image
## ~/about ## ~/about
- [ ] About me - [ ] About me
## ~/links ## ~/links
- [ ] Add Bluesky link
- [ ] Add MusicBrainz link ## ~/projects
- [ ] Add Archidekt link - [ ] 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 ## ~/slashes
- [ ] List of all slashes - [ ] List of all slashes
## ~/uses ## ~/uses
- [ ] Homelab uses - [x] Homelab uses
- [ ] Raspberry PI uses - [x] Raspberry PI uses
- [ ] Hardware anchor - [ ] Hardware anchor
## ~/certifications ## ~/certifications

View File

@ -1,6 +1,5 @@
// @ts-check // @ts-check
import { defineConfig, envField } from "astro/config" import { defineConfig, envField } from "astro/config"
import paraglide from "@inlang/paraglide-astro"
import tailwindcss from "@tailwindcss/vite" import tailwindcss from "@tailwindcss/vite"
import sitemap from "@astrojs/sitemap" import sitemap from "@astrojs/sitemap"
import svelte from "@astrojs/svelte" import svelte from "@astrojs/svelte"
@ -9,6 +8,7 @@ import mdx from "@astrojs/mdx"
import icon from "astro-icon" import icon from "astro-icon"
import { loadEnv } from "vite" import { loadEnv } from "vite"
import { paraglideVitePlugin } from "@inlang/paraglide-js"
const { URL } = process.env.NODE_ENV const { URL } = process.env.NODE_ENV
? loadEnv(process.env.NODE_ENV, process.cwd(), "") ? loadEnv(process.env.NODE_ENV, process.cwd(), "")
@ -20,30 +20,37 @@ export default defineConfig({
output: "server", output: "server",
i18n: { i18n: {
defaultLocale: "nb", defaultLocale: "nb",
locales: ["nb", "en"], locales: ["nb", "en"]
}, },
integrations: [ integrations: [
sitemap(), sitemap(),
mdx(), mdx(),
svelte(), svelte(),
icon(), icon()
paraglide({
project: "./project.inlang",
outdir: "./src/paraglide",
}),
], ],
adapter: node({ adapter: node({
mode: "standalone", mode: "standalone"
}), }),
vite: { vite: {
plugins: [tailwindcss()], plugins: [
tailwindcss(),
paraglideVitePlugin({
project: "./project.inlang",
outdir: "./src/paraglide"
})
]
},
markdown: {
shikiConfig: {
theme: "catppuccin-mocha"
}
}, },
env: { env: {
schema: { schema: {
DOMAIN: envField.string({ context: "client", access: "public" }), DOMAIN: envField.string({ context: "client", access: "public" }),
URL: envField.string({ context: "client", access: "public" }), URL: envField.string({ context: "client", access: "public" }),
GIT_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" })
}, }
}, }
}) })

57
biome.jsonc Normal file
View File

@ -0,0 +1,57 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": ["./src/paraglide"],
"include": ["./src/**"]
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "asNeeded",
"arrowParentheses": "asNeeded"
}
},
"overrides": [
{
"include": ["*.astro"],
"linter": {
"rules": {
"style": {
"useConst": "off",
"useImportType": "off"
}
}
}
},
{
"include": ["*.svelte"],
"linter": {
"rules": {
"style": {
"useConst": "off",
"useImportType": "off"
}
}
}
}
]
}

View File

@ -1,9 +1,20 @@
services: services:
web: prod:
container_name: martials.no hostname: martials.no
container_name: "martials.no"
restart: always restart: always
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
ports: ports:
- "4321:4321" - "4321:4321"
dev:
hostname: dev.martials.no
container_name: "dev.martials.no"
restart: always
build:
context: .
dockerfile: Dockerfile
ports:
- "4322:4321"

27
flake.lock generated Normal file
View File

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1751211869,
"narHash": "sha256-1Cu92i1KSPbhPCKxoiVG5qnoRiKTgR5CcGSRyLpOd7Y=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "b43c397f6c213918d6cfe6e3550abfe79b5d1c51",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

37
flake.nix Normal file
View File

@ -0,0 +1,37 @@
{
description = "martials.no Development environment";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
};
outputs = { nixpkgs, ... }:
let
system = "x86_64-linux";
in
{
devShells.${system}.default =
let
pkgs = import nixpkgs {
inherit system;
};
in
pkgs.mkShell {
packages = with pkgs; [
git
] ++ [
# Node
nodejs_22
pnpm
] ++ [
nodePackages.prettier
biome
];
shellHook = ''
pnpm install
fish
'';
};
};
}

View File

@ -1,5 +1,6 @@
{ {
"$schema": "https://inlang.com/schema/inlang-message-format", "$schema": "https://inlang.com/schema/inlang-message-format",
"archidektMessage": "My MTG cards and decks",
"hiIm": "Hi, I'm", "hiIm": "Hi, I'm",
"position": "Software Engineer", "position": "Software Engineer",
"aboutMe": "Dedicated developer currently working at Capgemini Bergen.", "aboutMe": "Dedicated developer currently working at Capgemini Bergen.",

View File

@ -1,5 +1,6 @@
{ {
"$schema": "https://inlang.com/schema/inlang-message-format", "$schema": "https://inlang.com/schema/inlang-message-format",
"archidektMessage": "Mine MTG kort og decks",
"hiIm": "Hei, jeg er", "hiIm": "Hei, jeg er",
"position": "Programvareutvikler", "position": "Programvareutvikler",
"aboutMe": "Engasjert utvikler som for tiden jobber hos Capgemini Bergen.", "aboutMe": "Engasjert utvikler som for tiden jobber hos Capgemini Bergen.",

View File

@ -10,48 +10,31 @@
"astro": "astro", "astro": "astro",
"type-check": "astro check", "type-check": "astro check",
"postinstall": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide", "postinstall": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide",
"format": "prettier --write \"./src/**/*.{js,mjs,ts,astro,svelte,css,md,json}\"", "format": "biome format --write .",
"lint": "biome lint --write .",
"lint:fix": "biome check --write .",
"watch-messages": "paraglide-js compile --watch --project ./project.inlang --outdir ./src/paraglide" "watch-messages": "paraglide-js compile --watch --project ./project.inlang --outdir ./src/paraglide"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.4",
"@astrojs/mdx": "^4.0.8", "@astrojs/mdx": "^4.3.0",
"@astrojs/node": "9.1.0", "@astrojs/node": "9.2.2",
"@astrojs/sitemap": "^3.2.1", "@astrojs/sitemap": "^3.4.1",
"@astrojs/svelte": "^7.0.4", "@astrojs/svelte": "^7.1.0",
"@iconify-json/pajamas": "^1.2.5", "@iconify-json/pajamas": "^1.2.11",
"@inlang/paraglide-astro": "^0.3.5", "@inlang/paraglide-js": "2.1.0",
"@inlang/paraglide-js": "1.11.8",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.0.6", "@tailwindcss/vite": "^4.1.11",
"astro": "5.3.0", "astro": "^5.10.2",
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"sharp": "^0.33.5", "dayjs": "^1.11.13",
"svelte": "^5.20.1", "sharp": "^0.34.2",
"tailwindcss": "^4.0.6", "svelte": "^5.34.9",
"typescript": "^5.7.3" "tailwindcss": "^4.1.11",
"typescript": "^5.8.3"
}, },
"devDependencies": { "devDependencies": {
"daisyui": "^5.0.0-beta.8", "daisyui": "^5.0.43",
"prettier": "^3.5.1", "vite": "^7.0.0"
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-svelte": "^3.3.3",
"vite": "^6.1.0"
},
"prettier": {
"semi": false,
"singleQuote": false,
"plugins": [
"prettier-plugin-astro",
"prettier-plugin-svelte"
],
"overrides": [
{
"files": "**/*.astro",
"options": {
"parser": "astro"
}
}
]
} }
} }

3067
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,15 @@
{ {
"$schema": "https://inlang.com/schema/project-settings", "$schema": "https://inlang.com/schema/project-settings",
"sourceLanguageTag": "nb", "baseLocale": "nb",
"languageTags": ["nb", "en"], "locales": [
"nb",
"en"
],
"modules": [ "modules": [
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js", "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js", "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js"
], ],
"plugin.inlang.messageFormat": { "plugin.inlang.messageFormat": {
"pathPattern": "./messages/{languageTag}.json" "pathPattern": "./messages/{locale}.json"
} }
} }

View File

@ -0,0 +1,39 @@
---
import { type NavLink } from "@/utils/linking"
import LocaleLink from "@/components/links/LocaleLink.astro"
import { deLocalizeHref } from "@/paraglide/runtime.js"
const pathname = deLocalizeHref(Astro.originPathname)
let paths: string[]
if (pathname === "/") {
paths = ["~"]
} else {
paths = ["~", ...pathname.split("/").filter(x => x)]
}
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

@ -2,11 +2,12 @@
import GiteaLink from "./links/GiteaLink.astro" import GiteaLink from "./links/GiteaLink.astro"
import PajamasIcon from "./icons/PajamasIcon.astro" import PajamasIcon from "./icons/PajamasIcon.astro"
import ExternalLink from "./links/ExternalLink.astro" import ExternalLink from "./links/ExternalLink.astro"
import LanguageButtonGroup from "./LanguageButtonGroup.astro" import LanguageButtonGroup from "./LanguageButtonGroup.svelte"
import { GIT_URL, STATUS_URL } from "astro:env/client" import { GIT_URL, STATUS_URL } from "astro:env/client"
import * as m from "@/paraglide/messages" import * as m from "@/paraglide/messages"
const giteaLink = `${GIT_URL}/martials/martials.no` const giteaLink = `${GIT_URL}/martials/martials.no`
const pathname = Astro.url.pathname
--- ---
<div class="divider bg-inherit"></div> <div class="divider bg-inherit"></div>
@ -28,5 +29,5 @@ const giteaLink = `${GIT_URL}/martials/martials.no`
{m.status()} {m.status()}
</ExternalLink> </ExternalLink>
</div> </div>
<LanguageButtonGroup /> <LanguageButtonGroup client:load />
</div> </div>

View File

@ -1,9 +1,10 @@
--- ---
import LocaleLink from "./links/LocaleLink.astro" import LocaleLink from "./links/LocaleLink.astro"
import { type NavLink, resolvePathname } from "@/utils/linking" import { type NavLink } from "@/utils/linking"
import { deLocalizeHref } from "@/paraglide/runtime"
const pathname = Astro.url.pathname const pathname = Astro.url.pathname
const currentPath = resolvePathname(pathname) const currentPath = deLocalizeHref(pathname)
const isEnglish = pathname.startsWith("/en") const isEnglish = pathname.startsWith("/en")
--- ---
@ -14,7 +15,7 @@ const isEnglish = pathname.startsWith("/en")
class:list={[ class:list={[
"btn join-item !text-cat-text border-cat-surface0", "btn join-item !text-cat-text border-cat-surface0",
!isEnglish ? "bg-cat-mantle" : "bg-cat-base", !isEnglish ? "bg-cat-mantle" : "bg-cat-base",
]}>Norsk</LocaleLink ]} client:load>Norsk</LocaleLink
> >
<LocaleLink <LocaleLink
to={currentPath as NavLink} to={currentPath as NavLink}
@ -22,6 +23,6 @@ const isEnglish = pathname.startsWith("/en")
class:list={[ class:list={[
"btn join-item !text-cat-text border-cat-surface0", "btn join-item !text-cat-text border-cat-surface0",
isEnglish ? "bg-cat-mantle" : "bg-cat-base", isEnglish ? "bg-cat-mantle" : "bg-cat-base",
]}>English</LocaleLink ]} client:load>English</LocaleLink
> >
</div> </div>

View File

@ -0,0 +1,29 @@
<script lang="ts">
import { getLocale, type Locale, setLocale } from "@/paraglide/runtime"
const isEnglish = getLocale() === "en"
function updateLocale(lang: Locale) {
setLocale(lang)
}
</script>
<div class="join">
<button
onclick={() => updateLocale("nb")}
class={[
"btn join-item !text-cat-text border-cat-surface0",
!isEnglish ? "bg-cat-mantle" : "bg-cat-base",
]}>Norsk
</button
>
<button
onclick={() => updateLocale("en")}
class={[
"btn join-item !text-cat-text border-cat-surface0",
isEnglish ? "bg-cat-mantle" : "bg-cat-base",
]}>English
</button
>
</div>

View File

@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
// TODO move to types? // TODO move to types?
interface Option<Key> { interface Option<Key> {
key: Key key: Key
value: string value: string
} }
interface Props<Key = string> { interface Props<Key = string> {
selected: Key selected: Key
options?: Option<Key>[] options?: Option<Key>[]
class?: string class?: string
} }
let { selected = $bindable(), options = [], class: clazz }: Props = $props() let { selected = $bindable(), options = [], class: clazz }: Props = $props()
</script> </script>
<select <select

View File

@ -1,80 +1,76 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte" import { onMount } from "svelte"
let history: string[] = [] let history: string[] = []
let currentDir = "~" let currentDir = "~"
type Command = "help" | "about" | "skills" | "projects" | "contact" | "clear" type Command = "help" | "about" | "skills" | "projects" | "contact" | "clear"
const commands: Record<Command, () => string> = { const commands: Record<Command, () => string> = {
help: () => `Available commands: help: () => `Available commands:
about - Display information about me about - Display information about me
skills - List my technical skills skills - List my technical skills
projects - Show my notable projects projects - Show my notable projects
contact - Display my contact information contact - Display my contact information
clear - Clear the terminal screen`, clear - Clear the terminal screen`,
about: () => `Hi, I'm John Doe! about: () => `Hi, I'm John Doe!
I'm a passionate software developer with 5 years of experience. I'm a passionate software developer with 5 years of experience.
I love creating elegant solutions to complex problems.`, I love creating elegant solutions to complex problems.`,
skills: () => `My technical skills include: skills: () => `My technical skills include:
- JavaScript/TypeScript - JavaScript/TypeScript
- React & Next.js - React & Next.js
- Node.js - Node.js
- Python - Python
- SQL & NoSQL databases`, - SQL & NoSQL databases`,
projects: () => `Some of my notable projects: projects: () => `Some of my notable projects:
1. E-commerce Platform (React, Node.js, MongoDB) 1. E-commerce Platform (React, Node.js, MongoDB)
2. Weather App (React Native, OpenWeatherMap API) 2. Weather App (React Native, OpenWeatherMap API)
3. Task Management System (Python, Django, PostgreSQL)`, 3. Task Management System (Python, Django, PostgreSQL)`,
contact: () => `You can reach me at: contact: () => `You can reach me at:
Email: john.doe@example.com Email: john.doe@example.com
GitHub: github.com/johndoe GitHub: github.com/johndoe
LinkedIn: linkedin.com/in/johndoe`, LinkedIn: linkedin.com/in/johndoe`,
clear: () => { clear: () => {
history = []
return ""
},
}
const executeCommand = (input: string) => {
const [command, ...args] = input.trim().split(" ")
if (command in commands) {
return commands[command as Command]()
}
return `Command not found: ${command}. Type 'help' for available commands.`
}
let input = ""
let inputRef: HTMLInputElement | null = null
const handleSubmit = (e: Event) => {
e.preventDefault()
if (input.trim()) {
if (input === "clear") {
history = [] history = []
return "" } else {
}, history = [...history, `${currentDir} $ ${input}`, executeCommand(input)]
}
const executeCommand = (input: string) => {
const [command, ...args] = input.trim().split(" ")
if (command in commands) {
return commands[command as Command]()
} }
return `Command not found: ${command}. Type 'help' for available commands.` input = ""
} }
}
let input = "" onMount(() => {
let inputRef: HTMLInputElement | null = null history = [
"Welcome to John Doe's Terminal Portfolio!",
"Type 'help' to see available commands.",
]
})
const handleSubmit = (e: Event) => { $: {
e.preventDefault() if (inputRef) {
if (input.trim()) { inputRef.scrollIntoView({ behavior: "smooth" })
if (input === "clear") {
history = []
} else {
history = [
...history,
`${currentDir} $ ${input}`,
executeCommand(input),
]
}
input = ""
}
}
onMount(() => {
history = [
"Welcome to John Doe's Terminal Portfolio!",
"Type 'help' to see available commands.",
]
})
$: {
if (inputRef) {
inputRef.scrollIntoView({ behavior: "smooth" })
}
} }
}
</script> </script>
<div class="min-h-screen bg-black text-green-500 p-4 font-mono"> <div class="min-h-screen bg-black text-green-500 p-4 font-mono">

View File

@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from "svelte" import type { Snippet } from "svelte"
interface Props { interface Props {
title?: string title?: string
children: Snippet children: Snippet
} }
const { title = "", children }: Props = $props() const { title = "", children }: Props = $props()
</script> </script>
<details class="collapse collapse-arrow bg-base-200"> <details class="collapse collapse-arrow bg-base-200">

View File

@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import Collapse from "@/components/collapse/Collapse.svelte" import Collapse from "@/components/collapse/Collapse.svelte"
interface Props { interface Props {
items?: string[] items?: string[]
title?: string title?: string
} }
const { items = [], title = "" }: Props = $props() const { items = [], title = "" }: Props = $props()
</script> </script>
<Collapse {title}> <Collapse {title}>

View File

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

View File

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

View File

@ -1,16 +1,17 @@
--- ---
import { languageTag, type AvailableLanguageTag } from "@/paraglide/runtime" import { localizeHref, getLocale, type Locale } from "@/paraglide/runtime"
import { localizePathname, type NavLink } from "@/utils/linking" import { type NavLink } from "@/utils/linking"
import type { ComponentProps } from "@/types/props" import type { ComponentProps } from "@/types/props"
interface Props extends ComponentProps { interface Props extends ComponentProps {
to: NavLink to: NavLink
lang?: AvailableLanguageTag lang?: Locale
} }
const { to, class: clazz, lang = languageTag() } = Astro.props const { to, class: clazz, lang = getLocale() } = Astro.props
--- ---
<a href={localizePathname(to, lang)} class={clazz}> <!-- TODO currently not working on Paraglide 2 https://github.com/opral/inlang-paraglide-js/issues/472 -->
<a href={localizeHref(to, { locale: lang })} class={clazz}>
<slot /> <slot />
</a> </a>

View File

@ -13,6 +13,15 @@ export interface MyLink {
} }
export default [ export default [
{
title: "Archidekt",
url: "https://archidekt.com/u/Emberal",
message: m.archidektMessage(),
},
{
title: "Bluesky",
url: "https://bsky.app/profile/martials.no",
},
{ {
title: "Codeberg", title: "Codeberg",
url: "https://codeberg.org/martials", url: "https://codeberg.org/martials",
@ -55,6 +64,10 @@ export default [
alt: "Mastodon icon", alt: "Mastodon icon",
}, },
}, },
{
title: "ListenBrainz",
url: "https://listenbrainz.org/user/emberal/",
},
{ {
title: "Pixelfed", title: "Pixelfed",
url: "https://pixelfed.social/i/web/profile/261454857934868480", url: "https://pixelfed.social/i/web/profile/261454857934868480",

View File

@ -1,12 +1,8 @@
--- ---
import { getCollection } from "astro:content"
import ProjectGrid from "./ProjectGrid.astro" import ProjectGrid from "./ProjectGrid.astro"
import { type CollectionEntry } from "astro:content"
interface Props { const projects = await getCollection("projects")
projects: CollectionEntry<"projects">[]
}
const { projects } = Astro.props
--- ---
<ProjectGrid projects={projects} /> <ProjectGrid projects={projects} />

View File

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

View File

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

View File

@ -2,14 +2,16 @@ import { defineCollection, z } from "astro:content"
import { glob } from "astro/loaders" import { glob } from "astro/loaders"
const projectCollection = defineCollection({ const projectCollection = defineCollection({
loader: glob({ pattern: "**\/*.mdx", base: "./src/content/projects" }), loader: glob({ pattern: "**/*.mdx", base: "./src/content/projects" }),
schema: ({ image }) => schema: ({ image }) =>
z.object({ z.object({
lang: z.union([z.literal("en"), z.literal("nb")]),
title: z.string(), title: z.string(),
description: z.string(), description: z.string(),
heroImage: image(), heroImage: image(),
heroImageAlt: z.string(), heroImageAlt: z.string(),
tags: z.array(z.string()), tags: z.array(z.string()),
keywords: z.array(z.string()),
source: z.string().url(), source: z.string().url(),
createdAt: z.string().date(), createdAt: z.string().date(),
updatedAt: z.string().date(), updatedAt: z.string().date(),
@ -17,7 +19,7 @@ const projectCollection = defineCollection({
}) })
const usesCollection = defineCollection({ const usesCollection = defineCollection({
loader: glob({ pattern: "**\/*.yaml", base: "./src/content/uses" }), loader: glob({ pattern: "**/*.yaml", base: "./src/content/uses" }),
schema: z.object({ schema: z.object({
title: z.string(), title: z.string(),
accessories: z.optional(z.array(z.string())), accessories: z.optional(z.array(z.string())),

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -1,9 +1,11 @@
--- ---
lang: "en"
title: "Welcome" title: "Welcome"
description: "Welcome to my homepage / portfolio" description: "Welcome to my homepage / portfolio"
heroImage: "assets/recursive-meme.png" heroImage: "assets/recursive-meme.png"
heroImageAlt: "A recursive meme that says: Self-reference, recursive meme is self-referential" heroImageAlt: "A recursive meme that says: Self-reference, recursive meme is self-referential"
tags: [Astro, Svelte, TypeScript, I18n, TailwindCSS, Docker] tags: [Astro, Svelte, TypeScript, I18n, TailwindCSS, Docker]
keywords: [Martin Berg Alstad, portfolio, homepage, website, martials, emberal]
source: "https://git.martials.no/martials/martials.no" source: "https://git.martials.no/martials/martials.no"
createdAt: "2024-09-22" createdAt: "2024-09-22"
updatedAt: "2025-02-15" updatedAt: "2025-02-15"

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,3 +1,3 @@
title: Homelab title: Homelab
hardware: hardware:
- b # Graphics cards, CPUs, etc. - 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

@ -1,5 +1,5 @@
title: Raspberry Pi 4 title: Raspberry Pi 4
accessories: accessories:
- a # Screens, keyboards, mice, etc. - 4 TB External harddrive
hardware: hardware:
- b # Graphics cards, CPUs, etc. - OKdo Raspberry Pi 4 Kit 4 GB # https://www.kjell.com/no/produkter/data/raspberry-pi/okdo-raspberry-pi-4-kit-4-gb-p88059

View File

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

6
src/middleware.ts Normal file
View File

@ -0,0 +1,6 @@
import { paraglideMiddleware } from "@/paraglide/server"
import { defineMiddleware } from "astro/middleware"
export const onRequest = defineMiddleware((context, next) => {
return paraglideMiddleware(context.request, () => next());
});

View File

@ -7,7 +7,10 @@ import "@/styles/global.css"
export const prerender = true export const prerender = true
export function getStaticPaths(): GetStaticPathsResult { export function getStaticPaths(): GetStaticPathsResult {
return [{ params: { project: "homepage" } }] return [
{ params: { project: "homepage" } },
{ params: { project: "sb1budget" } },
]
} }
const { project } = Astro.params const { project } = Astro.params

View File

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

View File

@ -7,7 +7,10 @@ import "@/styles/global.css"
export const prerender = true export const prerender = true
export function getStaticPaths(): GetStaticPathsResult { export function getStaticPaths(): GetStaticPathsResult {
return [{ params: { project: "homepage" } }] return [
{ params: { project: "homepage" } },
{ params: { project: "sb1budget" } },
]
} }
const { project } = Astro.params const { project } = Astro.params

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import type { AvailableLanguageTag } from "@/paraglide/runtime.js"
import type { AbsolutePathname, Project } from "@/types/types.ts" import type { AbsolutePathname, Project } from "@/types/types.ts"
interface TranslatedPathnames { interface TranslatedPathnames {
@ -36,30 +35,3 @@ for (const path of paths) {
en: `/en${path}`, en: `/en${path}`,
} }
} }
export function localizePathname(
pathname: NavLink,
locale: AvailableLanguageTag,
): string {
const pathnameParts = pathname.split("/")
const firstSegment: AbsolutePathname = `/${pathnameParts[1]}`
if (pathnames[firstSegment]) {
const localizedPathname = pathnames[firstSegment][locale]
const rest = pathnameParts.slice(2)
if (rest.length > 0) {
return `${localizedPathname}/${rest.join("/")}`
} else {
return localizedPathname
}
}
return pathname
}
export function resolvePathname(pathname: string): AbsolutePathname {
if (pathname.startsWith("/en")) {
return pathname.slice(3) as AbsolutePathname
}
return pathname as AbsolutePathname
}