Compare commits
21 Commits
v1.0.0
...
paraglide-
Author | SHA1 | Date | |
---|---|---|---|
051cadca66
|
|||
1114fe7565
|
|||
f11ed70b85
|
|||
bce965ba2a
|
|||
0337e34af5
|
|||
45226136f3
|
|||
a859439353
|
|||
969660abc8
|
|||
99aced7367
|
|||
78b333e9f7
|
|||
1d04befff1
|
|||
dc4d564059
|
|||
05ef06f95c
|
|||
097850267c
|
|||
9a82eba757
|
|||
ebb3db8645
|
|||
a2584b97a1
|
|||
14c65bda05
|
|||
16104d12ae
|
|||
83b2b9ac68
|
|||
8cc5c6971f
|
2
.env
2
.env
@ -1,3 +1,3 @@
|
||||
DOMAIN="martials.no"
|
||||
GIT_URL=https://git.$DOMAIN
|
||||
GIT_URL=https://code.$DOMAIN
|
||||
STATUS_URL="https://status.$DOMAIN/status/home"
|
@ -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
|
59
TODO.md
59
TODO.md
@ -1,46 +1,63 @@
|
||||
# TODO
|
||||
|
||||
## Code
|
||||
- [ ] day.js for dates
|
||||
- [ ] Nix Shell
|
||||
- [ ] License
|
||||
|
||||
## SSE
|
||||
- [x] Correct Sitemap.xml
|
||||
- [x] Correct robots.txt
|
||||
- [x] Correct security.txt
|
||||
## Code
|
||||
- [x] Nix Shell
|
||||
- [ ] Analytics
|
||||
- [ ] Organize code better
|
||||
- [ ] Type slug of project
|
||||
|
||||
## CI/CD
|
||||
- [ ] Staging environment
|
||||
- [ ] Deploy to staging environment on push to master
|
||||
- [ ] Deploy to production environment on push tag to master
|
||||
|
||||
## SEO
|
||||
- [ ] Meta tags on each page
|
||||
|
||||
## Layout
|
||||
- [x] Show current page
|
||||
- [x] Correct bg colour on entire page
|
||||
- [x] Hamburger menu on mobile
|
||||
- [ ] Dark mode toggle
|
||||
- [ ] Navigate using pathname / breadcrumbs
|
||||
- [x] Navigate using pathname / breadcrumbs
|
||||
- [ ] Better style for \<code /> blocks
|
||||
|
||||
## Accessibility
|
||||
- [x] Fix colours on buttons
|
||||
- [x] Correct contrast
|
||||
- [ ] All interactable elements have labels
|
||||
- [x] Colour links, also in MDX posts
|
||||
|
||||
## I18N
|
||||
- [ ] Markdown for translations
|
||||
|
||||
## ~/
|
||||
- [ ] About me description
|
||||
- [x] Latest projects
|
||||
- [x] Non-cat image
|
||||
- [ ] Limit latest projects to N (5?)
|
||||
|
||||
## ~/about
|
||||
- [ ] About me
|
||||
|
||||
## ~/links
|
||||
- [ ] Add Bluesky link
|
||||
- [ ] Add MusicBrainz link
|
||||
- [ ] Add Archidekt link
|
||||
|
||||
## ~/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
|
||||
- [ ] Homelab uses
|
||||
- [ ] Raspberry PI uses
|
||||
- [x] Homelab uses
|
||||
- [x] Raspberry PI uses
|
||||
- [ ] Hardware anchor
|
||||
|
||||
## ~/certifications
|
||||
|
@ -1,6 +1,5 @@
|
||||
// @ts-check
|
||||
import { defineConfig, envField } from "astro/config"
|
||||
import paraglide from "@inlang/paraglide-astro"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import sitemap from "@astrojs/sitemap"
|
||||
import svelte from "@astrojs/svelte"
|
||||
@ -9,6 +8,7 @@ import mdx from "@astrojs/mdx"
|
||||
import icon from "astro-icon"
|
||||
|
||||
import { loadEnv } from "vite"
|
||||
import { paraglideVitePlugin } from "@inlang/paraglide-js"
|
||||
|
||||
const { URL } = process.env.NODE_ENV
|
||||
? loadEnv(process.env.NODE_ENV, process.cwd(), "")
|
||||
@ -20,30 +20,37 @@ export default defineConfig({
|
||||
output: "server",
|
||||
i18n: {
|
||||
defaultLocale: "nb",
|
||||
locales: ["nb", "en"],
|
||||
locales: ["nb", "en"]
|
||||
},
|
||||
integrations: [
|
||||
sitemap(),
|
||||
mdx(),
|
||||
svelte(),
|
||||
icon(),
|
||||
paraglide({
|
||||
project: "./project.inlang",
|
||||
outdir: "./src/paraglide",
|
||||
}),
|
||||
icon()
|
||||
],
|
||||
adapter: node({
|
||||
mode: "standalone",
|
||||
mode: "standalone"
|
||||
}),
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
paraglideVitePlugin({
|
||||
project: "./project.inlang",
|
||||
outdir: "./src/paraglide"
|
||||
})
|
||||
]
|
||||
},
|
||||
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" })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
57
biome.jsonc
Normal file
57
biome.jsonc
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal 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
37
flake.nix
Normal 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
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"archidektMessage": "My MTG cards and decks",
|
||||
"hiIm": "Hi, I'm",
|
||||
"position": "Software Engineer",
|
||||
"aboutMe": "Dedicated developer currently working at Capgemini Bergen.",
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"archidektMessage": "Mine MTG kort og decks",
|
||||
"hiIm": "Hei, jeg er",
|
||||
"position": "Programvareutvikler",
|
||||
"aboutMe": "Engasjert utvikler som for tiden jobber hos Capgemini Bergen.",
|
||||
|
6
middleware.ts
Normal file
6
middleware.ts
Normal 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());
|
||||
});
|
53
package.json
53
package.json
@ -10,48 +10,31 @@
|
||||
"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}\"",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/mdx": "^4.0.8",
|
||||
"@astrojs/node": "9.1.0",
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"@astrojs/svelte": "^7.0.4",
|
||||
"@iconify-json/pajamas": "^1.2.5",
|
||||
"@inlang/paraglide-astro": "^0.3.5",
|
||||
"@inlang/paraglide-js": "1.11.8",
|
||||
"@astrojs/mdx": "^4.3.0",
|
||||
"@astrojs/node": "9.2.2",
|
||||
"@astrojs/sitemap": "^3.4.1",
|
||||
"@astrojs/svelte": "^7.1.0",
|
||||
"@iconify-json/pajamas": "^1.2.11",
|
||||
"@inlang/paraglide-js": "2.1.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.0.6",
|
||||
"astro": "5.3.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"astro": "^5.10.2",
|
||||
"astro-icon": "^1.1.5",
|
||||
"sharp": "^0.33.5",
|
||||
"svelte": "^5.20.1",
|
||||
"tailwindcss": "^4.0.6",
|
||||
"typescript": "^5.7.3"
|
||||
"dayjs": "^1.11.13",
|
||||
"sharp": "^0.34.2",
|
||||
"svelte": "^5.34.9",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"daisyui": "^5.0.0-beta.8",
|
||||
"prettier": "^3.5.1",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
"daisyui": "^5.0.43",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
}
|
||||
|
3067
pnpm-lock.yaml
generated
3067
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,15 +1,15 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/project-settings",
|
||||
"sourceLanguageTag": "nb",
|
||||
"languageTags": ["nb", "en"],
|
||||
"baseLocale": "nb",
|
||||
"locales": [
|
||||
"nb",
|
||||
"en"
|
||||
],
|
||||
"modules": [
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js",
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/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"
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
|
||||
],
|
||||
"plugin.inlang.messageFormat": {
|
||||
"pathPattern": "./messages/{languageTag}.json"
|
||||
"pathPattern": "./messages/{locale}.json"
|
||||
}
|
||||
}
|
||||
|
39
src/components/Breadcrumb.astro
Normal file
39
src/components/Breadcrumb.astro
Normal 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>
|
@ -1,9 +1,10 @@
|
||||
---
|
||||
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 currentPath = resolvePathname(pathname)
|
||||
const currentPath = deLocalizeHref(pathname)
|
||||
const isEnglish = pathname.startsWith("/en")
|
||||
---
|
||||
|
||||
|
@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
// TODO move to types?
|
||||
interface Option<Key> {
|
||||
key: Key
|
||||
value: string
|
||||
}
|
||||
// TODO move to types?
|
||||
interface Option<Key> {
|
||||
key: Key
|
||||
value: string
|
||||
}
|
||||
|
||||
interface Props<Key = string> {
|
||||
selected: Key
|
||||
options?: Option<Key>[]
|
||||
class?: string
|
||||
}
|
||||
interface Props<Key = string> {
|
||||
selected: Key
|
||||
options?: Option<Key>[]
|
||||
class?: string
|
||||
}
|
||||
|
||||
let { selected = $bindable(), options = [], class: clazz }: Props = $props()
|
||||
let { selected = $bindable(), options = [], class: clazz }: Props = $props()
|
||||
</script>
|
||||
|
||||
<select
|
||||
|
@ -1,80 +1,76 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let history: string[] = []
|
||||
let currentDir = "~"
|
||||
let history: string[] = []
|
||||
let currentDir = "~"
|
||||
|
||||
type Command = "help" | "about" | "skills" | "projects" | "contact" | "clear"
|
||||
type Command = "help" | "about" | "skills" | "projects" | "contact" | "clear"
|
||||
|
||||
const commands: Record<Command, () => string> = {
|
||||
help: () => `Available commands:
|
||||
const commands: Record<Command, () => string> = {
|
||||
help: () => `Available commands:
|
||||
about - Display information about me
|
||||
skills - List my technical skills
|
||||
projects - Show my notable projects
|
||||
contact - Display my contact information
|
||||
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 love creating elegant solutions to complex problems.`,
|
||||
skills: () => `My technical skills include:
|
||||
skills: () => `My technical skills include:
|
||||
- JavaScript/TypeScript
|
||||
- React & Next.js
|
||||
- Node.js
|
||||
- Python
|
||||
- SQL & NoSQL databases`,
|
||||
projects: () => `Some of my notable projects:
|
||||
projects: () => `Some of my notable projects:
|
||||
1. E-commerce Platform (React, Node.js, MongoDB)
|
||||
2. Weather App (React Native, OpenWeatherMap API)
|
||||
3. Task Management System (Python, Django, PostgreSQL)`,
|
||||
contact: () => `You can reach me at:
|
||||
contact: () => `You can reach me at:
|
||||
Email: john.doe@example.com
|
||||
GitHub: github.com/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 = []
|
||||
return ""
|
||||
},
|
||||
}
|
||||
|
||||
const executeCommand = (input: string) => {
|
||||
const [command, ...args] = input.trim().split(" ")
|
||||
if (command in commands) {
|
||||
return commands[command as Command]()
|
||||
} else {
|
||||
history = [...history, `${currentDir} $ ${input}`, executeCommand(input)]
|
||||
}
|
||||
return `Command not found: ${command}. Type 'help' for available commands.`
|
||||
input = ""
|
||||
}
|
||||
}
|
||||
|
||||
let input = ""
|
||||
let inputRef: HTMLInputElement | null = null
|
||||
onMount(() => {
|
||||
history = [
|
||||
"Welcome to John Doe's Terminal Portfolio!",
|
||||
"Type 'help' to see available commands.",
|
||||
]
|
||||
})
|
||||
|
||||
const handleSubmit = (e: Event) => {
|
||||
e.preventDefault()
|
||||
if (input.trim()) {
|
||||
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" })
|
||||
}
|
||||
$: {
|
||||
if (inputRef) {
|
||||
inputRef.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-black text-green-500 p-4 font-mono">
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte"
|
||||
import type { Snippet } from "svelte"
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
children: Snippet
|
||||
}
|
||||
interface Props {
|
||||
title?: string
|
||||
children: Snippet
|
||||
}
|
||||
|
||||
const { title = "", children }: Props = $props()
|
||||
const { title = "", children }: Props = $props()
|
||||
</script>
|
||||
|
||||
<details class="collapse collapse-arrow bg-base-200">
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import Collapse from "@/components/collapse/Collapse.svelte"
|
||||
import Collapse from "@/components/collapse/Collapse.svelte"
|
||||
|
||||
interface Props {
|
||||
items?: string[]
|
||||
title?: string
|
||||
}
|
||||
interface Props {
|
||||
items?: string[]
|
||||
title?: string
|
||||
}
|
||||
|
||||
const { items = [], title = "" }: Props = $props()
|
||||
const { items = [], title = "" }: Props = $props()
|
||||
</script>
|
||||
|
||||
<Collapse {title}>
|
||||
|
@ -2,13 +2,13 @@
|
||||
import PajamasIcon from "@/components/icons/PajamasIcon.astro"
|
||||
|
||||
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
|
||||
name="pajamas:hamburger"
|
||||
class="w-6 h-6"
|
||||
|
@ -2,22 +2,24 @@
|
||||
import Navbar from "./Navbar.astro"
|
||||
import NavbarDrawer from "./NavbarDrawer.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="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">
|
||||
<!-- Navbar -->
|
||||
<div class="navbar w-full justify-end">
|
||||
<div class="flex justify-between items-center w-full h-full sm:hidden">
|
||||
<h1 class="!text-2xl h-5">
|
||||
{currentPath}
|
||||
<h1 class="!text-xl h-5">
|
||||
<Breadcrumb />
|
||||
</h1>
|
||||
<HamburgerMenuButton id="my-drawer-3" />
|
||||
<HamburgerMenuButton for={drawerToggleId} />
|
||||
</div>
|
||||
<div class="hidden flex-none sm:block">
|
||||
<Navbar />
|
||||
@ -25,8 +27,11 @@ const currentPath = `~${resolvePathname(Astro.originPathname)}`
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<li class="text-xl font-bold my-5">
|
||||
{currentPath}
|
||||
|
@ -1,16 +1,16 @@
|
||||
---
|
||||
import { languageTag, type AvailableLanguageTag } from "@/paraglide/runtime"
|
||||
import { localizePathname, type NavLink } from "@/utils/linking"
|
||||
import { localizeHref, getLocale, type Locale } from "@/paraglide/runtime"
|
||||
import { type NavLink } from "@/utils/linking"
|
||||
import type { ComponentProps } from "@/types/props"
|
||||
|
||||
interface Props extends ComponentProps {
|
||||
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}>
|
||||
<a href={localizeHref(to, { locale: lang })} class={clazz}>
|
||||
<slot />
|
||||
</a>
|
||||
|
@ -13,6 +13,15 @@ export interface MyLink {
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
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",
|
||||
@ -55,6 +64,10 @@ export default [
|
||||
alt: "Mastodon icon",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "ListenBrainz",
|
||||
url: "https://listenbrainz.org/user/emberal/",
|
||||
},
|
||||
{
|
||||
title: "Pixelfed",
|
||||
url: "https://pixelfed.social/i/web/profile/261454857934868480",
|
||||
|
@ -1,12 +1,8 @@
|
||||
---
|
||||
import { getCollection } from "astro:content"
|
||||
import ProjectGrid from "./ProjectGrid.astro"
|
||||
import { type CollectionEntry } from "astro:content"
|
||||
|
||||
interface Props {
|
||||
projects: CollectionEntry<"projects">[]
|
||||
}
|
||||
|
||||
const { projects } = Astro.props
|
||||
const projects = await getCollection("projects")
|
||||
---
|
||||
|
||||
<ProjectGrid projects={projects} />
|
||||
|
@ -2,6 +2,7 @@
|
||||
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>
|
||||
@ -14,19 +15,26 @@ const baseUrl: NavLink = "/projects"
|
||||
|
||||
<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>
|
||||
),
|
||||
)
|
||||
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>
|
||||
|
@ -1,11 +1,12 @@
|
||||
---
|
||||
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 { getLocale } from "@/paraglide/runtime"
|
||||
import { getEntry, render } from "astro:content"
|
||||
import { Image } from "astro:assets"
|
||||
import * as m from "@/paraglide/messages"
|
||||
import dayjs from "dayjs"
|
||||
import "@/styles/global.css"
|
||||
|
||||
interface Props {
|
||||
@ -15,21 +16,38 @@ interface Props {
|
||||
const { project } = Astro.props
|
||||
|
||||
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 {
|
||||
lang,
|
||||
title,
|
||||
description,
|
||||
tags,
|
||||
keywords,
|
||||
heroImage,
|
||||
heroImageAlt,
|
||||
source,
|
||||
createdAt,
|
||||
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 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>
|
||||
<h2>{title}</h2>
|
||||
@ -37,10 +55,10 @@ const {
|
||||
</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>
|
||||
@ -49,5 +67,7 @@ const {
|
||||
<GiteaLink href={source} class="my-2" />
|
||||
|
||||
<p class="my-2">{description}</p>
|
||||
<Content />
|
||||
<div lang={lang}>
|
||||
<Content />
|
||||
</div>
|
||||
</Layout>
|
||||
|
@ -2,14 +2,16 @@ import { defineCollection, z } from "astro:content"
|
||||
import { glob } from "astro/loaders"
|
||||
|
||||
const projectCollection = defineCollection({
|
||||
loader: glob({ pattern: "**\/*.mdx", base: "./src/content/projects" }),
|
||||
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()),
|
||||
keywords: z.array(z.string()),
|
||||
source: z.string().url(),
|
||||
createdAt: z.string().date(),
|
||||
updatedAt: z.string().date(),
|
||||
@ -17,7 +19,7 @@ const projectCollection = defineCollection({
|
||||
})
|
||||
|
||||
const usesCollection = defineCollection({
|
||||
loader: glob({ pattern: "**\/*.yaml", base: "./src/content/uses" }),
|
||||
loader: glob({ pattern: "**/*.yaml", base: "./src/content/uses" }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
accessories: z.optional(z.array(z.string())),
|
||||
|
BIN
src/content/projects/assets/is_it_worth_the_time.png
Normal file
BIN
src/content/projects/assets/is_it_worth_the_time.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
@ -1,9 +1,11 @@
|
||||
---
|
||||
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: "2025-02-15"
|
||||
|
115
src/content/projects/sb1budget.mdx
Normal file
115
src/content/projects/sb1budget.mdx
Normal 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.
|
@ -1,3 +1,3 @@
|
||||
title: Homelab
|
||||
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
|
@ -1,5 +1,5 @@
|
||||
title: Raspberry Pi 4
|
||||
accessories:
|
||||
- a # Screens, keyboards, mice, etc.
|
||||
- 4 TB External harddrive
|
||||
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
|
@ -1,34 +1,39 @@
|
||||
---
|
||||
import Footer from "@/components/Footer.astro"
|
||||
import Header from "@/components/header/Header.astro"
|
||||
import { languageTag } from "@/paraglide/runtime"
|
||||
import { resolvePathname } from "@/utils/linking"
|
||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||
import { getLocale } 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"}>
|
||||
<html lang={getLocale()} dir={"ltr"}>
|
||||
<head>
|
||||
<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="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" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<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">
|
||||
~{resolvePathname(Astro.originPathname)}
|
||||
<Breadcrumb />
|
||||
</h1>
|
||||
<div class="my-5">
|
||||
<slot />
|
||||
|
@ -7,7 +7,10 @@ import "@/styles/global.css"
|
||||
export const prerender = true
|
||||
|
||||
export function getStaticPaths(): GetStaticPathsResult {
|
||||
return [{ params: { project: "homepage" } }]
|
||||
return [
|
||||
{ params: { project: "homepage" } },
|
||||
{ params: { project: "sb1budget" } },
|
||||
]
|
||||
}
|
||||
|
||||
const { project } = Astro.params
|
||||
|
@ -1,12 +1,9 @@
|
||||
---
|
||||
import { getCollection } from "astro:content"
|
||||
import Layout from "@/layouts/Layout.astro"
|
||||
import MyProjectsPage from "@/components/projects/MyProjectsPage.astro"
|
||||
import "@/styles/global.css"
|
||||
|
||||
const projects = await getCollection("projects")
|
||||
---
|
||||
|
||||
<Layout title="Projects">
|
||||
<MyProjectsPage projects={projects} />
|
||||
<MyProjectsPage />
|
||||
</Layout>
|
||||
|
@ -7,7 +7,10 @@ import "@/styles/global.css"
|
||||
export const prerender = true
|
||||
|
||||
export function getStaticPaths(): GetStaticPathsResult {
|
||||
return [{ params: { project: "homepage" } }]
|
||||
return [
|
||||
{ params: { project: "homepage" } },
|
||||
{ params: { project: "sb1budget" } },
|
||||
]
|
||||
}
|
||||
|
||||
const { project } = Astro.params
|
||||
|
@ -1,12 +1,9 @@
|
||||
---
|
||||
import { getCollection } from "astro:content"
|
||||
import Layout from "@/layouts/Layout.astro"
|
||||
import MyProjectsPage from "@/components/projects/MyProjectsPage.astro"
|
||||
import "@/styles/global.css"
|
||||
|
||||
const projects = await getCollection("projects")
|
||||
---
|
||||
|
||||
<Layout title="Prosjekter">
|
||||
<MyProjectsPage projects={projects} />
|
||||
<MyProjectsPage />
|
||||
</Layout>
|
||||
|
@ -4,50 +4,50 @@
|
||||
@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;
|
||||
--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 {
|
||||
@apply border border-red-500;
|
||||
}
|
||||
.debug {
|
||||
@apply border border-red-500;
|
||||
}
|
||||
}
|
||||
|
||||
br {
|
||||
@apply my-0.5;
|
||||
@apply my-0.5;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl font-bold mb-2;
|
||||
@apply text-4xl font-bold mb-2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-3xl font-bold mb-2;
|
||||
@apply text-3xl font-bold mb-2;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-2xl font-bold mb-2;
|
||||
@apply text-2xl font-bold mb-2;
|
||||
}
|
||||
|
||||
/* TODO change default style*/
|
||||
a {
|
||||
@apply link text-cat-mauve;
|
||||
text-decoration-line: none;
|
||||
@apply link text-cat-mauve;
|
||||
text-decoration-line: none;
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import type { AvailableLanguageTag } from "@/paraglide/runtime.js"
|
||||
import type { AbsolutePathname, Project } from "@/types/types.ts"
|
||||
|
||||
interface TranslatedPathnames {
|
||||
@ -36,30 +35,3 @@ for (const path of paths) {
|
||||
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
|
||||
}
|
||||
|
Reference in New Issue
Block a user