Compare commits
2 Commits
master
...
simplify_t
Author | SHA1 | Date | |
---|---|---|---|
b6a332a2fe | |||
4724b0a0e0 |
3
.env
@ -1,3 +0,0 @@
|
|||||||
DOMAIN="martials.no"
|
|
||||||
GIT_URL=https://git.$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 HOST=0.0.0.0
|
||||||
ENV PORT=4321
|
ENV PORT=4321
|
||||||
EXPOSE 4321
|
EXPOSE 4321
|
||||||
ENTRYPOINT node ./dist/server/entry.mjs
|
CMD node ./dist/server/entry.mjs
|
55
README.md
@ -1,5 +1,54 @@
|
|||||||
# Homepage / portfolio
|
# Astro Starter Kit: Basics
|
||||||
|
|
||||||
> This repository is mirrored on [Codeberg](https://codeberg.org/martials/martials.no)
|
```sh
|
||||||
|
npm create astro@latest -- --template basics
|
||||||
|
```
|
||||||
|
|
||||||
More to come!
|
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
|
||||||
|
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
|
||||||
|
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 🚀 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).
|
||||||
|
64
TODO.md
@ -1,64 +0,0 @@
|
|||||||
# 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
|
|
@ -1,7 +1,7 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import { defineConfig, envField } from "astro/config"
|
import { defineConfig, envField } from "astro/config"
|
||||||
import paraglide from "@inlang/paraglide-astro"
|
import paraglide from "@inlang/paraglide-astro"
|
||||||
import tailwindcss from "@tailwindcss/vite"
|
import tailwind from "@astrojs/tailwind"
|
||||||
import sitemap from "@astrojs/sitemap"
|
import sitemap from "@astrojs/sitemap"
|
||||||
import svelte from "@astrojs/svelte"
|
import svelte from "@astrojs/svelte"
|
||||||
import node from "@astrojs/node"
|
import node from "@astrojs/node"
|
||||||
@ -10,45 +10,36 @@ import icon from "astro-icon"
|
|||||||
|
|
||||||
import { loadEnv } from "vite"
|
import { loadEnv } from "vite"
|
||||||
|
|
||||||
const { URL } = process.env.NODE_ENV
|
const { url } = process.env.URL
|
||||||
? loadEnv(process.env.NODE_ENV, process.cwd(), "")
|
? loadEnv(process.env.URL, process.cwd(), "")
|
||||||
: { URL: "http://localhost:3000" }
|
: { url: "http://localhost:3000" }
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: URL,
|
site: url,
|
||||||
output: "server",
|
output: "server",
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: "nb",
|
defaultLocale: "nb",
|
||||||
locales: ["nb", "en"],
|
locales: ["nb", "en"]
|
||||||
},
|
},
|
||||||
integrations: [
|
integrations: [
|
||||||
|
tailwind(),
|
||||||
sitemap(),
|
sitemap(),
|
||||||
mdx(),
|
mdx(),
|
||||||
svelte(),
|
svelte(),
|
||||||
icon(),
|
icon(),
|
||||||
paraglide({
|
paraglide({
|
||||||
|
// recommended settings
|
||||||
project: "./project.inlang",
|
project: "./project.inlang",
|
||||||
outdir: "./src/paraglide",
|
outdir: "./src/paraglide" //where your files should be
|
||||||
}),
|
})
|
||||||
],
|
],
|
||||||
adapter: node({
|
adapter: node({
|
||||||
mode: "standalone",
|
mode: "standalone"
|
||||||
}),
|
}),
|
||||||
vite: {
|
|
||||||
plugins: [tailwindcss()],
|
|
||||||
},
|
|
||||||
markdown: {
|
|
||||||
shikiConfig: {
|
|
||||||
theme: "catppuccin-mocha",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
env: {
|
env: {
|
||||||
schema: {
|
schema: {
|
||||||
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" }),
|
}
|
||||||
STATUS_URL: envField.string({ context: "client", access: "public" }),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
@ -1,20 +1,17 @@
|
|||||||
{
|
{
|
||||||
"$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": "Some bullshit about me",
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"contactMe": "Contact me",
|
"contactMe": "Contact me",
|
||||||
"myLinks": "My links",
|
"myLinks": "My links",
|
||||||
"myProjects": "My projects",
|
"myProjects": "My projects",
|
||||||
"uses": "Uses",
|
|
||||||
"hardware": "Hardware",
|
"hardware": "Hardware",
|
||||||
"accessories": "Accessories",
|
"accessories": "Accessories",
|
||||||
"sourceCode": "Source code",
|
"sourceCode": "Source code",
|
||||||
"createdAt": "Created at",
|
"createdAt": "Created at",
|
||||||
"updatedAt": "Updated at",
|
"updatedAt": "Updated at",
|
||||||
"forMirrors": "For mirrors of Gitea",
|
|
||||||
"forPersonalProjects": "For personal projects",
|
"forPersonalProjects": "For personal projects",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
{
|
{
|
||||||
"$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": "Bunntekst",
|
||||||
"home": "Hjem",
|
"home": "Hjem",
|
||||||
"contactMe": "Kontakt meg",
|
"contactMe": "Kontakt meg",
|
||||||
"myLinks": "Mine lenker",
|
"myLinks": "Mine lenker",
|
||||||
"myProjects": "Mine prosjekter",
|
"myProjects": "Mine prosjekter",
|
||||||
"uses": "Uses",
|
|
||||||
"hardware": "Maskinvare",
|
"hardware": "Maskinvare",
|
||||||
"accessories": "Tilbehør",
|
"accessories": "Tilbehør",
|
||||||
"sourceCode": "Kildekode",
|
"sourceCode": "Kildekode",
|
||||||
"createdAt": "Opprettet",
|
"createdAt": "Opprettet",
|
||||||
"updatedAt": "Oppdatert",
|
"updatedAt": "Oppdatert",
|
||||||
"forMirrors": "For mirrors av Gitea",
|
|
||||||
"forPersonalProjects": "For personlige prosjekter",
|
"forPersonalProjects": "For personlige prosjekter",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"name": "Navn",
|
"name": "Navn",
|
||||||
|
40
package.json
@ -1,43 +1,43 @@
|
|||||||
{
|
{
|
||||||
"name": "martials-no-v2",
|
"name": "martials-no-v2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.0",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide && astro check && astro build",
|
"build": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide && astro check && astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"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": "prettier --write \"./src/**/*.{js,mjs,ts,astro,svelte,css,md,json}\"",
|
||||||
"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.0.0-beta.2",
|
||||||
"@astrojs/node": "9.1.1",
|
"@astrojs/node": "9.0.0-beta.2",
|
||||||
"@astrojs/sitemap": "^3.2.1",
|
"@astrojs/sitemap": "^3.2.1",
|
||||||
"@astrojs/svelte": "^7.0.4",
|
"@astrojs/svelte": "6.0.0-beta.1",
|
||||||
"@iconify-json/pajamas": "^1.2.5",
|
"@astrojs/tailwind": "^5.1.2",
|
||||||
"@inlang/paraglide-astro": "^0.3.5",
|
"@iconify-json/pajamas": "^1.2.3",
|
||||||
"@inlang/paraglide-js": "1.11.8",
|
"@inlang/paraglide-astro": "^0.2.2",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@inlang/paraglide-js": "1.11.2",
|
||||||
"@tailwindcss/vite": "^4.0.9",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"astro": "^5.4.1 ",
|
"astro": "5.0.0-beta.5",
|
||||||
"astro-icon": "^1.1.5",
|
"astro-icon": "^1.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"diff": "^7.0.0",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"svelte": "^5.20.4",
|
"svelte": "^4.2.19",
|
||||||
"tailwindcss": "^4.0.9",
|
"tailwindcss": "^3.4.13",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"daisyui": "^5.0.0",
|
"@types/diff": "^5.2.3",
|
||||||
"prettier": "^3.5.2",
|
"daisyui": "^4.12.13",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
"prettier-plugin-svelte": "^3.3.3",
|
"prettier-plugin-svelte": "^3.2.7",
|
||||||
"vite": "^6.2.0"
|
"vite": "^5.4.8"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"semi": false,
|
"semi": false,
|
||||||
|
2824
pnpm-lock.yaml
generated
Before Width: | Height: | Size: 55 KiB |
9
public/favicon.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 749 B |
@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
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>
|
|
@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
import Input from "@/components/Input.astro"
|
|
||||||
import * as m from "@/paraglide/messages.js"
|
|
||||||
// TODO self-host email server
|
|
||||||
---
|
|
||||||
|
|
||||||
<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>
|
|
@ -1,30 +1,19 @@
|
|||||||
---
|
---
|
||||||
import GiteaLink from "./links/GiteaLink.astro"
|
import GiteaLink from "./links/GiteaLink.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.astro"
|
||||||
import { GIT_URL, STATUS_URL } from "astro:env/client"
|
|
||||||
import * as m from "@/paraglide/messages"
|
import * as m from "@/paraglide/messages"
|
||||||
|
import { LINKED_IN_URL, THIS_GIT_URL, STATUS_URL } from "../constants"
|
||||||
const giteaLink = `${GIT_URL}/martials/martials.no`
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="divider bg-inherit"></div>
|
<div class="divider" />
|
||||||
<div
|
<div class="py-5 flex flex-row gap-1 justify-around w-full items-center">
|
||||||
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 class="flex flex-col gap-1">
|
||||||
>
|
<GiteaLink href={THIS_GIT_URL} />
|
||||||
<div>
|
<ExternalLink href={LINKED_IN_URL} iconLeft="pajamas:linkedin" iconLeftAriaLabel="LinkedIn" title="LinkedIn">
|
||||||
<GiteaLink href={giteaLink} class="!text-cat-text" />
|
LinkedIn
|
||||||
<ExternalLink
|
</ExternalLink>
|
||||||
href={STATUS_URL}
|
<ExternalLink href={STATUS_URL} iconLeft="pajamas:status-health" iconLeftAriaLabel="Status health" title="Status">
|
||||||
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()}
|
{m.status()}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
import { Image } from "astro:assets"
|
import { Image } from "astro:assets"
|
||||||
import me from "@/images/me.jpg"
|
import me from "@/images/polite-cat.jpg"
|
||||||
import * as m from "@/paraglide/messages.js"
|
import * as m from "@/paraglide/messages.js"
|
||||||
import "@/styles/global.css"
|
import "@/styles/global.css"
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="flex items-center justify-around flex-wrap">
|
<div class="flex items-center justify-around flex-wrap">
|
||||||
<div class="m-5">
|
<div class="m-5">
|
||||||
<h1>
|
<h1 class="sm:text-7xl font-bold">
|
||||||
{m.hiIm()}
|
{m.hiIm()}
|
||||||
<br />
|
<br />
|
||||||
Martin Berg Alstad
|
Martin Berg Alstad
|
||||||
@ -16,14 +16,7 @@ import "@/styles/global.css"
|
|||||||
</h1>
|
</h1>
|
||||||
<p class="mx-1 sm:my-10">{m.aboutMe()}</p>
|
<p class="mx-1 sm:my-10">{m.aboutMe()}</p>
|
||||||
</div>
|
</div>
|
||||||
<Image
|
<Image src={me} alt="Me on a hike" width="400" height="400" class="p-5 mx-auto" loading={"eager"} />
|
||||||
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>
|
</div>
|
||||||
<!-- Mastodon verification -->
|
<!-- 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>
|
35
src/components/HardwarePage.svelte
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Select from "./Select.svelte"
|
||||||
|
import * as m from "@/paraglide/messages"
|
||||||
|
import CollapseList from "@/components/collapse/CollapseList.svelte"
|
||||||
|
import type { CollectionEntry } from "astro:content"
|
||||||
|
|
||||||
|
export let hardware: CollectionEntry<"hardware">[] = []
|
||||||
|
|
||||||
|
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>
|
@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
interface Props {
|
|
||||||
label: string
|
|
||||||
type?: "text" | "email" | "password" | "number"
|
|
||||||
name: string
|
|
||||||
required?: boolean
|
|
||||||
placeholder?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
label,
|
|
||||||
type = "text",
|
|
||||||
name,
|
|
||||||
required = false,
|
|
||||||
placeholder,
|
|
||||||
} = Astro.props
|
|
||||||
---
|
|
||||||
|
|
||||||
<label class="flex flex-col w-full">
|
|
||||||
{label}
|
|
||||||
<input
|
|
||||||
class="input input-bordered w-full bg-cat-base"
|
|
||||||
type={type}
|
|
||||||
name={name}
|
|
||||||
required={required}
|
|
||||||
placeholder={placeholder}
|
|
||||||
/>
|
|
||||||
</label>
|
|
19
src/components/Input.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let label: string
|
||||||
|
export let type: "text" | "email" | "password" | "number" = "text"
|
||||||
|
export let name: string
|
||||||
|
export let required: boolean = false
|
||||||
|
export let placeholder: string
|
||||||
|
export let inputClass: string
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label class="flex flex-col ${$$restProps.class}">
|
||||||
|
{label}
|
||||||
|
<input
|
||||||
|
class="input input-bordered ${inputClass}"
|
||||||
|
type={type}
|
||||||
|
name={name}
|
||||||
|
required={required}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
</label>
|
@ -2,26 +2,10 @@
|
|||||||
import LocaleLink from "./links/LocaleLink.astro"
|
import LocaleLink from "./links/LocaleLink.astro"
|
||||||
import { type NavLink, resolvePathname } from "@/utils/linking"
|
import { type NavLink, resolvePathname } from "@/utils/linking"
|
||||||
|
|
||||||
const pathname = Astro.url.pathname
|
const currentPath = resolvePathname(Astro.url.pathname)
|
||||||
const currentPath = resolvePathname(pathname)
|
|
||||||
const isEnglish = pathname.startsWith("/en")
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="join">
|
<div class="join">
|
||||||
<LocaleLink
|
<LocaleLink to={currentPath as NavLink} lang="nb" class="btn join-item">Norsk</LocaleLink>
|
||||||
to={currentPath as NavLink}
|
<LocaleLink to={currentPath as NavLink} lang="en" class="btn join-item">English</LocaleLink>
|
||||||
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>
|
</div>
|
||||||
|
7
src/components/Menu.svelte
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<!-- TODO -->
|
||||||
|
</div>
|
14
src/components/Navbar.astro
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
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>
|
7
src/components/Row.svelte
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center ${$$restProps.class}">
|
||||||
|
<slot />
|
||||||
|
</div>
|
@ -1,22 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
// TODO move to types?
|
// TODO move to types?
|
||||||
interface Option<Key> {
|
interface Option {
|
||||||
key: Key
|
key: string
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props<Key = string> {
|
export let options: Option[] = []
|
||||||
selected: Key
|
// TODO bind data instead of dispatching events
|
||||||
options?: Option<Key>[]
|
const dispatch = createEventDispatcher<{ change: string }>()
|
||||||
class?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
let { selected = $bindable(), options = [], class: clazz }: Props = $props()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
bind:value={selected}
|
class="select select-bordered w-full max-w-xs ${$$restProps.class}"
|
||||||
class="select select-bordered w-full max-w-xs ${clazz}"
|
on:change={(value) => dispatch("change", value.currentTarget.value)}
|
||||||
>
|
>
|
||||||
{#each options as { key, value }}
|
{#each options as { key, value }}
|
||||||
<option value={key}>{value}</option>
|
<option value={key}>{value}</option>
|
||||||
|
29
src/components/Switch.svelte
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
export let defaultValue: boolean = false
|
||||||
|
export let title: string
|
||||||
|
export let onChange: (value: boolean) => void
|
||||||
|
export let name: string
|
||||||
|
export let id: string
|
||||||
|
|
||||||
|
let checked = defaultValue
|
||||||
|
|
||||||
|
function handleChange() {
|
||||||
|
checked = !checked
|
||||||
|
if (onChange) {
|
||||||
|
onChange(checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id={id}
|
||||||
|
on:click={handleChange}
|
||||||
|
title={title}
|
||||||
|
class={`${checked ? "bg-cyan-900" : "bg-gray-500"} relative my-2 inline-flex h-6 w-11 items-center rounded-full ${$$restProps.class}`}
|
||||||
|
>
|
||||||
|
<span class={"sr-only"}>{name}</span>
|
||||||
|
<span
|
||||||
|
class={`${checked ? "translate-x-6" : "translate-x-1"} inline-block h-4 w-4 transform rounded-full bg-white transition-all`}
|
||||||
|
/>
|
||||||
|
</button>
|
@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
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>
|
|
@ -5,4 +5,4 @@ interface Props {
|
|||||||
const { tag } = Astro.props
|
const { tag } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="badge badge-outline !text-cat-lavender">{tag}</div>
|
<div class="badge badge-outline">{tag}</div>
|
||||||
|
@ -7,6 +7,6 @@ interface Props {
|
|||||||
const { tags } = Astro.props
|
const { tags } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-1 py-0.5">
|
<div class="flex flex-wrap gap-1">
|
||||||
{tags.map((tag) => <Badge tag={tag} />)}
|
{tags.map((tag) => <Badge tag={tag} />)}
|
||||||
</div>
|
</div>
|
||||||
|
11
src/components/buttons/Button.svelte
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
export let id: string | undefined = undefined
|
||||||
|
export let title: string
|
||||||
|
export let type: "button" | "submit" | "reset" = "button"
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button id={id} class="btn" title={title} type={type}>
|
||||||
|
<slot />
|
||||||
|
</button>
|
@ -1,17 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from "svelte"
|
export let title: string = ""
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title?: string
|
|
||||||
children: Snippet
|
|
||||||
}
|
|
||||||
|
|
||||||
const { title = "", children }: Props = $props()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<details class="collapse collapse-arrow bg-base-200">
|
<details class="collapse collapse-arrow bg-base-200">
|
||||||
<summary class="collapse-title text-xl font-medium">{title}</summary>
|
<summary class="collapse-title text-xl font-medium">{title}</summary>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
{@render children()}
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
@ -1,15 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Collapse from "@/components/collapse/Collapse.svelte"
|
import Collapse from "@/components/collapse/Collapse.svelte"
|
||||||
|
|
||||||
interface Props {
|
export let items: string[] = []
|
||||||
items?: string[]
|
export let title: string = ""
|
||||||
title?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const { items = [], title = "" }: Props = $props()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Collapse {title}>
|
<Collapse title={title}>
|
||||||
<ul>
|
<ul>
|
||||||
{#each items as item}
|
{#each items as item}
|
||||||
<li class="list-disc ml-5">{item}</li>
|
<li class="list-disc ml-5">{item}</li>
|
||||||
|
17
src/components/contactMe/ContactMeForm.astro
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
import Input from "@/components/Input.svelte"
|
||||||
|
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>
|
7
src/components/contactMe/ContactMePage.astro
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
import ContactMeForm from "./ContactMeForm.astro"
|
||||||
|
import * as m from "@/paraglide/messages"
|
||||||
|
---
|
||||||
|
|
||||||
|
<h1 class="text-center">{m.contactMe()}</h1>
|
||||||
|
<ContactMeForm />
|
@ -1,17 +0,0 @@
|
|||||||
---
|
|
||||||
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>
|
|
@ -1,43 +0,0 @@
|
|||||||
---
|
|
||||||
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>
|
|
@ -1,17 +0,0 @@
|
|||||||
---
|
|
||||||
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>
|
|
@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
import Links from "../../links"
|
|
||||||
import LocaleLink from "../links/LocaleLink.astro"
|
|
||||||
---
|
|
||||||
|
|
||||||
{
|
|
||||||
Links.map((link) => (
|
|
||||||
<li>
|
|
||||||
<LocaleLink to={link.to}>{link.label}</LocaleLink>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
}
|
|
10
src/components/icons/Gitea.astro
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
import PajamasIcon from "./PajamasIcon.astro"
|
||||||
|
interface Props {
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { class: clazz } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<PajamasIcon name="pajamas:gitea" aria-label="Gitea" class={clazz} />
|
@ -2,11 +2,13 @@
|
|||||||
import type { PajamasIcon } from "@/types/icons"
|
import type { PajamasIcon } from "@/types/icons"
|
||||||
import type { ComponentProps } from "@/types/props"
|
import type { ComponentProps } from "@/types/props"
|
||||||
import { Icon } from "astro-icon/components"
|
import { Icon } from "astro-icon/components"
|
||||||
|
|
||||||
interface Props extends ComponentProps {
|
interface Props extends ComponentProps {
|
||||||
name: PajamasIcon
|
name: PajamasIcon
|
||||||
|
"aria-label": string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, class: clazz, ...props } = Astro.props
|
const { name, class: clazz, ...props } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<Icon name={name} class:list={[clazz]} {...props} />
|
<Icon name={name} class:list={["w-6 h-6", clazz]} {...props} />
|
||||||
|
10
src/components/icons/PajamasIcon.svelte
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PajamasIcon } from "@/types/icons"
|
||||||
|
import { Icon } from "astro-icon/components"
|
||||||
|
|
||||||
|
export let name: PajamasIcon
|
||||||
|
export let ariaLabel: string
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Icon name={name} class="w-6 h-6 ${$$restProps.class}" aria-label={ariaLabel} />
|
@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
import Greeting from "./Greeting.astro"
|
|
||||||
import LatestProjects from "./LatestProjects.astro"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Greeting />
|
|
||||||
<LatestProjects />
|
|
@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
import { getCollection } from "astro:content"
|
|
||||||
import ProjectGrid from "@/components/projects/ProjectGrid.astro"
|
|
||||||
|
|
||||||
const projects = await getCollection("projects")
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<ProjectGrid projects={projects} />
|
|
@ -1,18 +1,28 @@
|
|||||||
---
|
---
|
||||||
import type { LinkProps } from "@/types/props"
|
import type { LinkProps } from "@/types/props"
|
||||||
|
import type { PajamasIcon } from "@/types/icons"
|
||||||
|
import ExternalLinkTextOnly from "./ExternalLinkTextOnly.astro"
|
||||||
|
import ExternalLinkIconLeft from "./ExternalLinkIconLeft.astro"
|
||||||
|
|
||||||
interface Props extends LinkProps {
|
interface Props extends LinkProps {
|
||||||
noStyle?: boolean
|
iconLeft?: PajamasIcon
|
||||||
|
iconLeftAriaLabel?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { href, noStyle = false, class: clazz, ...props } = Astro.props
|
const { iconLeft, iconLeftAriaLabel, ...props } = Astro.props
|
||||||
|
if (iconLeft && !iconLeftAriaLabel) {
|
||||||
|
throw new Error("ExternalLink: iconLeftAriaLabel is required when iconLeft is provided")
|
||||||
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<a
|
{ iconLeft && iconLeftAriaLabel
|
||||||
href={href}
|
?
|
||||||
target="_blank"
|
<ExternalLinkIconLeft iconLeft={iconLeft} iconLeftAriaLabel={iconLeftAriaLabel} {...props}>
|
||||||
rel="noopener"
|
|
||||||
class:list={[noStyle ? "" : "link", clazz]}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</a>
|
</ExternalLinkIconLeft>
|
||||||
|
:
|
||||||
|
<ExternalLinkTextOnly {...props}>
|
||||||
|
<slot />
|
||||||
|
</ExternalLinkTextOnly>
|
||||||
|
}
|
||||||
|
|
||||||
|
20
src/components/links/ExternalLinkIconLeft.astro
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
import type { PajamasIcon as PajamasIconType } from "@/types/icons"
|
||||||
|
import type { LinkProps } from "@/types/props"
|
||||||
|
import ExternalLinkTextOnly from "./ExternalLinkTextOnly.astro"
|
||||||
|
import PajamasIcon from "../icons/PajamasIcon.astro"
|
||||||
|
|
||||||
|
interface Props extends LinkProps {
|
||||||
|
iconLeft: PajamasIconType
|
||||||
|
iconLeftAriaLabel: string
|
||||||
|
}
|
||||||
|
const { href, class: clazz, iconLeft, iconLeftAriaLabel, ...props } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<PajamasIcon name={iconLeft} aria-label={iconLeftAriaLabel} class="mr-2" />
|
||||||
|
<ExternalLinkTextOnly href={href} class={clazz} {...props}>
|
||||||
|
<slot />
|
||||||
|
</ExternalLinkTextOnly>
|
||||||
|
</div>
|
||||||
|
|
12
src/components/links/ExternalLinkTextOnly.astro
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
import type { LinkProps } from "@/types/props"
|
||||||
|
|
||||||
|
interface Props extends LinkProps {
|
||||||
|
}
|
||||||
|
|
||||||
|
const { href, class: clazz, ...props } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<a href={href} target="_blank" rel="noopener" class:list={[clazz]} {...props}>
|
||||||
|
<slot />
|
||||||
|
</a>
|
@ -1,21 +1,14 @@
|
|||||||
---
|
---
|
||||||
import ExternalLink from "./ExternalLink.astro"
|
import ExternalLink from "./ExternalLink.astro"
|
||||||
import * as m from "@/paraglide/messages"
|
import * as m from "@/paraglide/messages"
|
||||||
import PajamasIcon from "../icons/PajamasIcon.astro"
|
|
||||||
interface Props {
|
interface Props {
|
||||||
href: string
|
href: string
|
||||||
class?: string
|
|
||||||
}
|
}
|
||||||
const { href, class: clazz } = Astro.props
|
const { href } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ExternalLink
|
<ExternalLink iconLeft="pajamas:gitea" iconLeftAriaLabel="Gitea" href={href}>
|
||||||
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()}
|
{m.sourceCode()}
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,27 +8,12 @@ interface Props extends MyLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { title, message, url, icon, class: clazz } = Astro.props
|
const { title, message, url, icon, class: clazz } = Astro.props
|
||||||
const iconStyle = "w-6 h-6"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<ExternalLink href={url} noStyle>
|
<ExternalLink href={url}>
|
||||||
<div class:list={["card bg-cat-mantle text-cat-text", clazz]}>
|
<div class:list={["card bg-neutral", clazz]}>
|
||||||
<div class="card-body p-5 flex flex-row items-center">
|
<div class="card-body p-5 flex flex-row items-center">
|
||||||
{
|
<PajamasIcon name={icon ?? "pajamas:link"} aria-label={icon ? title : "Link"} />
|
||||||
icon ? (
|
|
||||||
<PajamasIcon
|
|
||||||
name={icon.src}
|
|
||||||
class={iconStyle}
|
|
||||||
aria-label={icon.alt}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PajamasIcon
|
|
||||||
name={"pajamas:link"}
|
|
||||||
class={iconStyle}
|
|
||||||
aria-label="Link icon"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<div>
|
<div>
|
||||||
<h5 class="card-title">{title}</h5>
|
<h5 class="card-title">{title}</h5>
|
||||||
<p class="prose">{message}</p>
|
<p class="prose">{message}</p>
|
||||||
|
@ -2,8 +2,10 @@
|
|||||||
import links from "./myLinks"
|
import links from "./myLinks"
|
||||||
import LinkCard from "./LinkCard.astro"
|
import LinkCard from "./LinkCard.astro"
|
||||||
import "@/styles/global.css"
|
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">
|
<div class="flex flex-col mx-auto w-fit gap-5">
|
||||||
{
|
{
|
||||||
links.map((link) => (
|
links.map((link) => (
|
||||||
|
@ -1,83 +1,46 @@
|
|||||||
import type { PajamasIcon } from "@/types/icons.ts"
|
import type { PajamasIcon } from "@/types/icons.ts"
|
||||||
import { GIT_URL } from "astro:env/client"
|
|
||||||
import * as m from "@/paraglide/messages"
|
import * as m from "@/paraglide/messages"
|
||||||
|
import * as c from "@/constants.ts"
|
||||||
|
|
||||||
export interface MyLink {
|
export interface MyLink {
|
||||||
title: string
|
title: string;
|
||||||
url: string
|
url: string;
|
||||||
message?: string
|
message?: string;
|
||||||
icon?: {
|
icon?: PajamasIcon
|
||||||
src: PajamasIcon
|
|
||||||
alt: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
title: "Archidekt",
|
title: "GitHub",
|
||||||
url: "https://archidekt.com/u/Emberal",
|
url: c.GITHUB_PROFILE_URL,
|
||||||
message: m.archidektMessage(),
|
icon: "pajamas:github"
|
||||||
},
|
|
||||||
{
|
|
||||||
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",
|
title: "Gitea",
|
||||||
url: `${GIT_URL}/martials`,
|
url: c.GIT_PROFILE_URL,
|
||||||
message: m.forPersonalProjects(),
|
message: m.forPersonalProjects(),
|
||||||
icon: {
|
icon: "pajamas:gitea"
|
||||||
src: "pajamas:gitea",
|
|
||||||
alt: "Gitea icon",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "GitHub",
|
|
||||||
url: "https://github.com/emberal",
|
|
||||||
icon: {
|
|
||||||
src: "pajamas:github",
|
|
||||||
alt: "GitHub icon",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "LinkedIn",
|
title: "LinkedIn",
|
||||||
url: "https://www.linkedin.com/in/martin-b-2a69391a3/",
|
url: c.LINKED_IN_URL,
|
||||||
icon: {
|
icon: "pajamas:linkedin"
|
||||||
src: "pajamas:linkedin",
|
|
||||||
alt: "LinkedIn icon",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Mastodon (Snabelen.no)",
|
title: "Mastodon (Snabelen)",
|
||||||
url: "https://snabelen.no/@Martials",
|
url: c.MASTODON_URL,
|
||||||
icon: {
|
icon: "pajamas:mastodon"
|
||||||
src: "pajamas:mastodon",
|
|
||||||
alt: "Mastodon icon",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "ListenBrainz",
|
|
||||||
url: "https://listenbrainz.org/user/emberal/",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Pixelfed",
|
title: "Pixelfed",
|
||||||
url: "https://pixelfed.social/i/web/profile/261454857934868480",
|
url: c.PIXELFED_URL
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Steam",
|
title: "Steam",
|
||||||
url: "https://steamcommunity.com/id/martials/",
|
url: c.STEAM_URL
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Trakt.tv",
|
title: "Trakt.tv",
|
||||||
url: "https://trakt.tv/users/martials",
|
url: c.TRAKT_URL
|
||||||
},
|
}
|
||||||
] satisfies MyLink[]
|
] satisfies MyLink[]
|
||||||
|
@ -1,8 +1,37 @@
|
|||||||
---
|
---
|
||||||
import { getCollection } from "astro:content"
|
import ProjectCard from "./ProjectCard.astro"
|
||||||
import ProjectGrid from "./ProjectGrid.astro"
|
import * as m from "@/paraglide/messages"
|
||||||
|
import { type CollectionEntry } from "astro:content"
|
||||||
|
import { type NavLink } from "@/utils/linking"
|
||||||
|
|
||||||
const projects = await getCollection("projects")
|
interface Props {
|
||||||
|
projects: CollectionEntry<"projects">[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { projects } = Astro.props
|
||||||
|
|
||||||
|
const baseUrl: NavLink = "/projects"
|
||||||
---
|
---
|
||||||
|
|
||||||
<ProjectGrid projects={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>
|
@ -19,13 +19,12 @@ const { title, description, tags, image, imageAlt, linkTo } = Astro.props
|
|||||||
|
|
||||||
<LocaleLink
|
<LocaleLink
|
||||||
to={linkTo}
|
to={linkTo}
|
||||||
class="card bg-cat-base max-w-96 shadow-xl hover:scale-105 transition border border-cat-surface0"
|
class="card bg-base-100 max-w-96 shadow-xl hover:scale-105 transition"
|
||||||
>
|
>
|
||||||
<figure>
|
<figure>
|
||||||
<Image src={image} alt={imageAlt} />
|
<Image src={image} alt={imageAlt} />
|
||||||
<figcaption class="sr-only">{imageAlt}</figcaption>
|
|
||||||
</figure>
|
</figure>
|
||||||
<div class="card-body text-cat-text">
|
<div class="card-body">
|
||||||
<h2 class="card-title">
|
<h2 class="card-title">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
---
|
|
||||||
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>
|
|
@ -1,12 +1,11 @@
|
|||||||
---
|
---
|
||||||
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 GiteaLink from "@/components/links/GiteaLink.astro"
|
|
||||||
import { languageTag } from "@/paraglide/runtime"
|
|
||||||
import { getEntry, render } from "astro:content"
|
|
||||||
import { Image } from "astro:assets"
|
import { Image } from "astro:assets"
|
||||||
import dayjs from "dayjs"
|
import { getEntry, render } from "astro:content"
|
||||||
|
import BadgeList from "../badge/BadgeList.astro"
|
||||||
|
import * as m from "@/paraglide/messages"
|
||||||
|
import { languageTag } from "@/paraglide/runtime"
|
||||||
|
import GiteaLink from "../links/GiteaLink.astro"
|
||||||
import "@/styles/global.css"
|
import "@/styles/global.css"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -16,55 +15,39 @@ 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!)
|
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 (languageTag() === "nb") {
|
|
||||||
template = "DD/MM/YYYY"
|
|
||||||
}
|
|
||||||
return dayjs(isoString).locale(languageTag()).format(template)
|
|
||||||
}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<!--TODO day.js for dates?-->
|
||||||
title={title}
|
<Layout title={title} class="mx-auto max-w-[750px]">
|
||||||
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>
|
<h1>{title}</h1>
|
||||||
<BadgeList tags={tags} />
|
<BadgeList tags={tags} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-end">
|
<div class="flex flex-col items-end">
|
||||||
<p>
|
<p>
|
||||||
{m.createdAt()}: {localeDateString(createdAt)}
|
{m.createdAt()}: {new Date(createdAt).toLocaleDateString(languageTag())}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{m.updatedAt()}: {localeDateString(updatedAt)}
|
{m.updatedAt()}: {new Date(updatedAt).toLocaleDateString(languageTag())}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Image src={heroImage} alt={heroImageAlt} class="m-auto" />
|
<Image src={heroImage} alt={heroImageAlt} />
|
||||||
|
|
||||||
<GiteaLink href={source} class="my-2" />
|
<GiteaLink href={source} />
|
||||||
|
|
||||||
<p class="my-2">{description}</p>
|
<p class="my-2">{description}</p>
|
||||||
<div lang={lang}>
|
|
||||||
<Content />
|
<Content />
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
26
src/components/simplifyTruths/HowTo.astro
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
import KeywordsDisclosure from "./KeywordsDisclosure.astro"
|
||||||
|
import DisclosureContainer from "./output/DisclosureContainer.astro"
|
||||||
|
import Disclosure from "./output/Disclosure.astro"
|
||||||
|
import ExternalLink from "../links/ExternalLink.astro"
|
||||||
|
// TODO translate and move link
|
||||||
|
---
|
||||||
|
|
||||||
|
<DisclosureContainer>
|
||||||
|
<Disclosure title={"How to"}>
|
||||||
|
<p>
|
||||||
|
Fill in a truth expression and it will be simplified for you as much as possible. It will
|
||||||
|
also genereate a truth table with all possible values. You can use a single letter, word or
|
||||||
|
multiple words without spacing for each atomic value. If you do not want to simplify the
|
||||||
|
expression, simply turn off the toggle. Keywords for operators are defined below.
|
||||||
|
Parentheses is also allowed.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
API docs can be found
|
||||||
|
<ExternalLink href={"https://api.martials.no/simplify-truths"}>here</ExternalLink>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</Disclosure>
|
||||||
|
|
||||||
|
<KeywordsDisclosure />
|
||||||
|
</DisclosureContainer>
|
40
src/components/simplifyTruths/KeywordsDisclosure.astro
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
import Disclosure from "./output/Disclosure.astro"
|
||||||
|
// TODO Translate
|
||||||
|
---
|
||||||
|
|
||||||
|
<Disclosure title={"Keywords"}>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class={"text-left"}>
|
||||||
|
<th>Name</th>
|
||||||
|
<th class={"pr-2"}>API</th>
|
||||||
|
<th>Other</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Not:</td>
|
||||||
|
<td>!</td>
|
||||||
|
<td>NOT</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>And:</td>
|
||||||
|
<td>&</td>
|
||||||
|
<td>AND</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Or:</td>
|
||||||
|
<td>|</td>
|
||||||
|
<td>/</td>
|
||||||
|
<td>OR</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={"pr-2"}>Implication:</td>
|
||||||
|
<td>{"->"}</td>
|
||||||
|
<td class={"px-2"}>IMPLICATION</td>
|
||||||
|
<td>IMP</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Disclosure>
|
24
src/components/simplifyTruths/Search.svelte
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import Input from "@/components/Input.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
inputClass={`rounded-xl pl-7 h-10 w-full pr-8`}
|
||||||
|
class={`w-full ${$$restProps.class}`}
|
||||||
|
id={id}
|
||||||
|
ref={ref}
|
||||||
|
placeholder="¬A & B -> C"
|
||||||
|
type={"text"}
|
||||||
|
onChange={onChange}
|
||||||
|
leading={
|
||||||
|
<Icon path={magnifyingGlass} aria-label={"Magnifying glass"} class={"absolute pl-2"} />
|
||||||
|
}
|
||||||
|
trailing={
|
||||||
|
<Show when={typing()} keyed>
|
||||||
|
<button class={"absolute right-2"} title={"Clear"} type={"reset"} onClick={clearSearch}>
|
||||||
|
<Icon path={xMark} aria-label={"The letter X"} />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
/>
|
37
src/components/simplifyTruths/ShowMeHow.astro
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
import type { FetchResult } from "@/types/types"
|
||||||
|
import Disclosure from "./output/Disclosure.astro"
|
||||||
|
import DisclosureContainer from "./output/DisclosureContainer.astro"
|
||||||
|
import { diffChars } from "diff"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
fetchResult: FetchResult | null
|
||||||
|
}
|
||||||
|
const { fetchResult } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<DisclosureContainer>
|
||||||
|
<Disclosure title={"Show me how it's done"}>
|
||||||
|
<table class={"table"}>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
fetchResult?.orderOperations?.map((operation, index) => (
|
||||||
|
<tr class={"border-b border-dotted border-gray-500"}>
|
||||||
|
<td>{index + 1}:</td>
|
||||||
|
<td class={"px-2"}>
|
||||||
|
{
|
||||||
|
diffChars(operation.before, operation.after).map((part) => (
|
||||||
|
<span class={`${part.added && "bg-green-700"} ${part.removed && "bg-red-700"}`}>
|
||||||
|
{part.value}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>using: {operation.law}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Disclosure>
|
||||||
|
</DisclosureContainer>
|
10
src/components/simplifyTruths/SimplifyTruthsPage.astro
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
import SimplifyTruthsPageBody from "./SimplifyTruthsPageBody.svelte"
|
||||||
|
import HowTo from "./HowTo.astro"
|
||||||
|
---
|
||||||
|
|
||||||
|
<div id={"truth-content"}>
|
||||||
|
<SimplifyTruthsPageBody>
|
||||||
|
<HowTo />
|
||||||
|
</SimplifyTruthsPageBody>
|
||||||
|
</div>
|
428
src/components/simplifyTruths/SimplifyTruthsPage.tsx
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
/* @refresh reload */
|
||||||
|
|
||||||
|
type Option = {
|
||||||
|
name: string
|
||||||
|
value: "NONE" | "TRUE" | "FALSE" | "DEFAULT" | "TRUE_FIRST" | "FALSE_FIRST"
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUrls = [
|
||||||
|
"http://localhost:8080/simplify/table/",
|
||||||
|
"https://api.martials.no/simplify-truths/simplify/table/"
|
||||||
|
]
|
||||||
|
|
||||||
|
// TODO move some code to new components
|
||||||
|
const TruthTablePage: Component = () => {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
let inputElement: HTMLInputElement | undefined = undefined
|
||||||
|
|
||||||
|
let simplifyDefault = searchParams.simplify === undefined || searchParams.simplify === "true",
|
||||||
|
inputContent = !!searchParams.exp,
|
||||||
|
hideIntermediate = searchParams.hideIntermediate === "true"
|
||||||
|
|
||||||
|
const [simplifyEnabled, setSimplifyEnabled] = createSignal(simplifyDefault)
|
||||||
|
const [fetchResult, setFetchResult] = createSignal<FetchResult | null>(null)
|
||||||
|
|
||||||
|
const hideOptions: Option[] = [
|
||||||
|
{ name: "Show all result", value: "NONE" },
|
||||||
|
{ name: "Hide true results", value: "TRUE" },
|
||||||
|
{ name: "Hide false results", value: "FALSE" }
|
||||||
|
]
|
||||||
|
|
||||||
|
const [hideValues, setHideValues] = createSignal(hideOptions[0])
|
||||||
|
|
||||||
|
const sortOptions: Option[] = [
|
||||||
|
{ name: "Sort by default", value: "DEFAULT" },
|
||||||
|
{ name: "Sort by true first", value: "TRUE_FIRST" },
|
||||||
|
{ name: "Sort by false first", value: "FALSE_FIRST" }
|
||||||
|
]
|
||||||
|
|
||||||
|
const [sortValues, setSortValues] = createSignal(sortOptions[0])
|
||||||
|
const [hideIntermediates, setHideIntermediates] = createSignal(hideIntermediate)
|
||||||
|
const [isLoaded, setIsLoaded] = createSignal<boolean | null>(null)
|
||||||
|
const [error, setError] = createSignal<{ title: string; message: string } | null>(null)
|
||||||
|
const [useLocalhost, setUseLocalhost] = createSignal(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the state of the current expression to the new search with all whitespace removed.
|
||||||
|
* If the element is not found, reset.
|
||||||
|
*/
|
||||||
|
function onClick(e: Event): void {
|
||||||
|
e.preventDefault() // Stops the page from reloading onClick
|
||||||
|
const exp = inputElement?.value
|
||||||
|
|
||||||
|
if (exp) {
|
||||||
|
setSearchParams({
|
||||||
|
exp,
|
||||||
|
simplify: simplifyEnabled(),
|
||||||
|
hide: hideValues().value,
|
||||||
|
sort: sortValues().value,
|
||||||
|
hideIntermediate: hideIntermediates()
|
||||||
|
})
|
||||||
|
|
||||||
|
getFetchResult(exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFetchResult(exp: string | null): void {
|
||||||
|
setFetchResult(null)
|
||||||
|
|
||||||
|
if (exp && exp !== "") {
|
||||||
|
exp = replaceOperators(exp)
|
||||||
|
setError(null)
|
||||||
|
setIsLoaded(false)
|
||||||
|
|
||||||
|
fetch(`${fetchUrls[useLocalhost() ? 0 : 1]}${encodeURIComponent(exp)}?
|
||||||
|
simplify=${simplifyEnabled()}&hide=${hideValues().value}&sort=${sortValues().value}&caseSensitive=false&
|
||||||
|
hideIntermediate=${hideIntermediates()}`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status !== "OK" && !res.ok) {
|
||||||
|
return setError({ title: "Input error", message: res.message })
|
||||||
|
}
|
||||||
|
return setFetchResult(res)
|
||||||
|
})
|
||||||
|
.catch((err) => setError({ title: "Fetch error", message: err.toString() }))
|
||||||
|
.finally(() => setIsLoaded(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount((): void => {
|
||||||
|
if (searchParams.exp) {
|
||||||
|
const exp = searchParams.exp
|
||||||
|
if (exp && inputElement) {
|
||||||
|
inputElement.value = exp
|
||||||
|
}
|
||||||
|
const hide = searchParams.hide
|
||||||
|
if (hide) {
|
||||||
|
setHideValues(hideOptions.find((o) => o.value === hide) ?? hideOptions[0])
|
||||||
|
}
|
||||||
|
const sort = searchParams.sort
|
||||||
|
if (sort) {
|
||||||
|
setSortValues(sortOptions.find((o) => o.value === sort) ?? sortOptions[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
getFetchResult(exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focuses searchbar on load
|
||||||
|
if (!isTouch()) {
|
||||||
|
inputElement?.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableId = "truth-table"
|
||||||
|
const filenameId = "excel-filename"
|
||||||
|
|
||||||
|
function _exportToExcel(): void {
|
||||||
|
const value = getElementById<HTMLInputElement>(filenameId)?.value
|
||||||
|
exportToExcel({
|
||||||
|
name: value !== "" ? value : undefined,
|
||||||
|
tableId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout title={"Truth tables"}>
|
||||||
|
<Show when={import.meta.env.DEV ?? false} keyed>
|
||||||
|
(DEV) Use localhost:
|
||||||
|
<MySwitch title={"Use localhost"} defaultValue={false} onChange={setUseLocalhost} />
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div id={"truth-content"}>
|
||||||
|
<div class={"mx-auto max-w-2xl"}>
|
||||||
|
<HowTo />
|
||||||
|
|
||||||
|
<form class={"flex-row-center"} onSubmit={onClick} autocomplete={"off"}>
|
||||||
|
<Search ref={inputElement} typingDefault={inputContent} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
id={"truth-input-button"}
|
||||||
|
title={"Generate (Enter)"}
|
||||||
|
type={"submit"}
|
||||||
|
className={"min-w-50px ml-2 h-10"}
|
||||||
|
children={"Generate"}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Options row */}
|
||||||
|
<Row className={"my-1 gap-2"}>
|
||||||
|
<span class={"h-min"}>{"Simplify"}: </span>
|
||||||
|
|
||||||
|
<MySwitch
|
||||||
|
onChange={setSimplifyEnabled}
|
||||||
|
defaultValue={simplifyEnabled()}
|
||||||
|
title={"Simplify"}
|
||||||
|
name={"Turn on/off simplify expressions"}
|
||||||
|
className={"mx-1"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class={"relative h-min"}>
|
||||||
|
<MyMenu
|
||||||
|
title={"Filter results"}
|
||||||
|
id={"filter-results"}
|
||||||
|
button={
|
||||||
|
<Show
|
||||||
|
when={hideValues().value !== "NONE"}
|
||||||
|
children={
|
||||||
|
<Icon
|
||||||
|
path={eyeSlash}
|
||||||
|
aria-label={"An eye with a slash through it"}
|
||||||
|
class={`mx-1 ${hideValues().value === "TRUE" ? "text-green-500" : "text-red-500"}`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fallback={<Icon path={eye} aria-label={"An eye"} class={"mx-1"} />}
|
||||||
|
keyed
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
children={
|
||||||
|
<For each={hideOptions}>
|
||||||
|
{(option) => (
|
||||||
|
<SingleMenuItem
|
||||||
|
onClick={() => setHideValues(option)}
|
||||||
|
option={option}
|
||||||
|
currentValue={hideValues}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
}
|
||||||
|
itemsClassName={"right-0"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class={"relative h-min"}>
|
||||||
|
<MyMenu
|
||||||
|
title={"Sort results"}
|
||||||
|
id={"sort-results"}
|
||||||
|
button={
|
||||||
|
<Icon
|
||||||
|
path={funnel}
|
||||||
|
aria-label={"Filter"}
|
||||||
|
class={`h-6 w-6 ${
|
||||||
|
sortValues().value === "TRUE_FIRST"
|
||||||
|
? "text-green-500"
|
||||||
|
: sortValues().value === "FALSE_FIRST" && "text-red-500"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
children={
|
||||||
|
<For each={sortOptions}>
|
||||||
|
{(option) => (
|
||||||
|
<SingleMenuItem
|
||||||
|
option={option}
|
||||||
|
currentValue={sortValues}
|
||||||
|
onClick={() => setSortValues(option)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
}
|
||||||
|
itemsClassName={"right-0"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MySwitch
|
||||||
|
title={"Hide intermediate values"}
|
||||||
|
onChange={setHideIntermediates}
|
||||||
|
defaultValue={hideIntermediates()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Show when={isLoaded() && error() === null} keyed>
|
||||||
|
<MyDialog
|
||||||
|
title={"Download"}
|
||||||
|
description={"Export current table (.xlsx)"}
|
||||||
|
button={
|
||||||
|
<>
|
||||||
|
<p class={"sr-only"}>{"Download"}</p>
|
||||||
|
<Icon aria-label={"Download"} path={arrowDownTray} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
callback={_exportToExcel}
|
||||||
|
acceptButtonName={"Download"}
|
||||||
|
cancelButtonName={"Cancel"}
|
||||||
|
buttonClass={`float-right`}
|
||||||
|
buttonTitle={"Export current table"}
|
||||||
|
acceptButtonId={"download-accept"}
|
||||||
|
>
|
||||||
|
<p>{"Filename"}:</p>
|
||||||
|
<Input
|
||||||
|
className={"border-rounded h-10 px-2"}
|
||||||
|
id={filenameId}
|
||||||
|
placeholder={"Truth Table"}
|
||||||
|
/>
|
||||||
|
</MyDialog>
|
||||||
|
</Show>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Show when={error()} keyed>
|
||||||
|
<ErrorBox
|
||||||
|
title={error()?.title ?? "Error"}
|
||||||
|
error={error()?.message ?? "Something went wrong"}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={isLoaded() === false} keyed>
|
||||||
|
<Icon
|
||||||
|
path={arrowPath}
|
||||||
|
aria-label={"Loading indicator"}
|
||||||
|
class={"mx-auto animate-spin"}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={simplifyEnabled() && (fetchResult()?.orderOperations?.length ?? 0) > 0} keyed>
|
||||||
|
<ShowMeHow fetchResult={fetchResult} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={isLoaded() && error() === null} keyed>
|
||||||
|
<Show when={simplifyEnabled()} keyed>
|
||||||
|
<InfoBox
|
||||||
|
className={"mx-auto w-fit pb-1 text-center text-lg"}
|
||||||
|
title={"Output:"}
|
||||||
|
id={"expression-output"}
|
||||||
|
>
|
||||||
|
<p>{fetchResult()?.after}</p>
|
||||||
|
</InfoBox>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class={"m-2 flex justify-center"}>
|
||||||
|
<div id={"table"} class={"h-[45rem] overflow-auto"}>
|
||||||
|
<TruthTable
|
||||||
|
header={fetchResult()?.header ?? undefined}
|
||||||
|
table={fetchResult()?.table?.truthMatrix}
|
||||||
|
id={tableId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TruthTablePage
|
||||||
|
|
||||||
|
interface SingleMenuItem {
|
||||||
|
option: Option
|
||||||
|
currentValue?: Accessor<Option>
|
||||||
|
onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>
|
||||||
|
}
|
||||||
|
|
||||||
|
const SingleMenuItem: Component<SingleMenuItem> = ({ option, currentValue, onClick }) => {
|
||||||
|
const isSelected = () => currentValue?.().value === option.value
|
||||||
|
return (
|
||||||
|
<button class={`flex-row-center cursor-pointer last:mb-1 hover:underline`} onClick={onClick}>
|
||||||
|
<Icon
|
||||||
|
path={check}
|
||||||
|
aria-label={isSelected() ? "A checkmark" : "Nothing"}
|
||||||
|
class={`text-white ${!isSelected() && "invisible"}`}
|
||||||
|
/>
|
||||||
|
{option.name}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorBox: Component<{ title: string; error: string }> = ({ title, error }) => (
|
||||||
|
<InfoBox className={"mx-auto w-fit text-center"} title={title} error={true}>
|
||||||
|
<p>{error}</p>
|
||||||
|
</InfoBox>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface ShowMeHowProps {
|
||||||
|
fetchResult: Accessor<FetchResult | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShowMeHow: Component<ShowMeHowProps> = ({ fetchResult }) => (
|
||||||
|
<MyDisclosureContainer>
|
||||||
|
<MyDisclosure title={"Show me how it's done"}>
|
||||||
|
<table class={"table"}>
|
||||||
|
<tbody>
|
||||||
|
<For each={fetchResult()?.orderOperations}>{orderOperationRow()}</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</MyDisclosure>
|
||||||
|
</MyDisclosureContainer>
|
||||||
|
)
|
||||||
|
|
||||||
|
const HowTo: Component = () => (
|
||||||
|
<MyDisclosureContainer>
|
||||||
|
<MyDisclosure title={"How to"}>
|
||||||
|
<p>
|
||||||
|
Fill in a truth expression and it will be simplified for you as much as possible. It will
|
||||||
|
also genereate a truth table with all possible values. You can use a single letter, word or
|
||||||
|
multiple words without spacing for each atomic value. If you do not want to simplify the
|
||||||
|
expression, simply turn off the toggle. Keywords for operators are defined below.
|
||||||
|
Parentheses is also allowed.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
API docs can be found <Link to={"https://api.martials.no/simplify-truths"}>here</Link>.
|
||||||
|
</p>
|
||||||
|
</MyDisclosure>
|
||||||
|
|
||||||
|
<KeywordsDisclosure />
|
||||||
|
</MyDisclosureContainer>
|
||||||
|
)
|
||||||
|
|
||||||
|
const orderOperationRow = () => (operation: OrderOfOperation, index: Accessor<number>) => (
|
||||||
|
<tr class={"border-b border-dotted border-gray-500"}>
|
||||||
|
<td>{index() + 1}:</td>
|
||||||
|
<td class={"px-2"}>
|
||||||
|
{
|
||||||
|
<For each={diffChars(operation.before, operation.after)}>
|
||||||
|
{(part) => (
|
||||||
|
<span class={`${part.added && "bg-green-700"} ${part.removed && "bg-red-700"}`}>
|
||||||
|
{part.value}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Show when={typeof window !== "undefined" && window.outerWidth <= 640} keyed>
|
||||||
|
<p>
|
||||||
|
{"using"}: {operation.law}
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
</td>
|
||||||
|
<Show when={typeof window !== "undefined" && window.outerWidth > 640} keyed>
|
||||||
|
<td>
|
||||||
|
{"using"}: {operation.law}
|
||||||
|
</td>
|
||||||
|
</Show>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
|
||||||
|
const KeywordsDisclosure: Component = () => (
|
||||||
|
<MyDisclosure title={"Keywords"}>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class={"text-left"}>
|
||||||
|
<th>Name</th>
|
||||||
|
<th class={"pr-2"}>API</th>
|
||||||
|
<th>Other</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Not:</td>
|
||||||
|
<td>!</td>
|
||||||
|
<td>NOT</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>And:</td>
|
||||||
|
<td>&</td>
|
||||||
|
<td>AND</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Or:</td>
|
||||||
|
<td>|</td>
|
||||||
|
<td>/</td>
|
||||||
|
<td>OR</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={"pr-2"}>Implication:</td>
|
||||||
|
<td>{"->"}</td>
|
||||||
|
<td class={"px-2"}>IMPLICATION</td>
|
||||||
|
<td>IMP</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</MyDisclosure>
|
||||||
|
)
|
302
src/components/simplifyTruths/SimplifyTruthsPageBody.svelte
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import Switch from "@/components/Switch.svelte"
|
||||||
|
import Search from "@/components/simplifyTruths/Search.svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import Button from "@/components/buttons/Button.svelte"
|
||||||
|
import Row from "@/components/Row.svelte"
|
||||||
|
import Menu from "@/components/Menu.svelte"
|
||||||
|
import PajamasIcon from "@/components/icons/PajamasIcon.svelte"
|
||||||
|
|
||||||
|
let useLocalhost = false
|
||||||
|
let inputElement: HTMLInputElement | null = null
|
||||||
|
|
||||||
|
// TODO refactor getter and setters
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the state of the current expression to the new search with all whitespace removed.
|
||||||
|
* If the element is not found, reset.
|
||||||
|
*/
|
||||||
|
function onFormSubmit(e: Event): void {
|
||||||
|
e.preventDefault() // Stops the page from reloading on:click
|
||||||
|
const exp = inputElement?.value // TODO test
|
||||||
|
|
||||||
|
if (exp) {
|
||||||
|
setSearchParams({
|
||||||
|
exp,
|
||||||
|
simplify: simplifyEnabled(),
|
||||||
|
hide: hideValues().value,
|
||||||
|
sort: sortValues().value,
|
||||||
|
hideIntermediate: hideIntermediates()
|
||||||
|
})
|
||||||
|
|
||||||
|
getFetchResult(exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFetchResult(exp: string | null): void {
|
||||||
|
setFetchResult(null)
|
||||||
|
|
||||||
|
if (exp && exp !== "") {
|
||||||
|
exp = replaceOperators(exp)
|
||||||
|
setError(null)
|
||||||
|
setIsLoaded(false)
|
||||||
|
|
||||||
|
// TODO refactor
|
||||||
|
fetch(`${fetchUrls[useLocalhost() ? 0 : 1]}${encodeURIComponent(exp)}?
|
||||||
|
simplify=${simplifyEnabled()}&hide=${hideValues().value}&sort=${sortValues().value}&caseSensitive=false&
|
||||||
|
hideIntermediate=${hideIntermediates()}`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status !== "OK" && !res.ok) {
|
||||||
|
return setError({ title: "Input error", message: res.message })
|
||||||
|
}
|
||||||
|
return setFetchResult(res)
|
||||||
|
})
|
||||||
|
.catch((err) => setError({ title: "Fetch error", message: err.toString() }))
|
||||||
|
.finally(() => setIsLoaded(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUrls = [
|
||||||
|
"http://localhost:8080/simplify/table/",
|
||||||
|
"https://api.martials.no/simplify-truths/simplify/table/"
|
||||||
|
]
|
||||||
|
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
let inputElement: HTMLInputElement | undefined = undefined
|
||||||
|
|
||||||
|
let simplifyDefault = searchParams.simplify === undefined || searchParams.simplify === "true",
|
||||||
|
inputContent = !!searchParams.exp,
|
||||||
|
hideIntermediate = searchParams.hideIntermediate === "true"
|
||||||
|
|
||||||
|
const [simplifyEnabled, setSimplifyEnabled] = createSignal(simplifyDefault)
|
||||||
|
const [fetchResult, setFetchResult] = createSignal<FetchResult | null>(null)
|
||||||
|
|
||||||
|
const hideOptions: Option[] = [
|
||||||
|
{ name: "Show all result", value: "NONE" },
|
||||||
|
{ name: "Hide true results", value: "TRUE" },
|
||||||
|
{ name: "Hide false results", value: "FALSE" }
|
||||||
|
]
|
||||||
|
|
||||||
|
const [hideValues, setHideValues] = createSignal(hideOptions[0])
|
||||||
|
|
||||||
|
const sortOptions: Option[] = [
|
||||||
|
{ name: "Sort by default", value: "DEFAULT" },
|
||||||
|
{ name: "Sort by true first", value: "TRUE_FIRST" },
|
||||||
|
{ name: "Sort by false first", value: "FALSE_FIRST" }
|
||||||
|
]
|
||||||
|
|
||||||
|
const [sortValues, setSortValues] = createSignal(sortOptions[0])
|
||||||
|
const [hideIntermediates, setHideIntermediates] = createSignal(hideIntermediate)
|
||||||
|
const [isLoaded, setIsLoaded] = createSignal<boolean | null>(null)
|
||||||
|
const [error, setError] = createSignal<{ title: string; message: string } | null>(null)
|
||||||
|
const [useLocalhost, setUseLocalhost] = createSignal(false)
|
||||||
|
|
||||||
|
onMount((): void => {
|
||||||
|
if (searchParams.exp) {
|
||||||
|
const exp = searchParams.exp
|
||||||
|
if (exp && inputElement) {
|
||||||
|
inputElement.value = exp
|
||||||
|
}
|
||||||
|
const hide = searchParams.hide
|
||||||
|
if (hide) {
|
||||||
|
setHideValues(hideOptions.find((o) => o.value === hide) ?? hideOptions[0])
|
||||||
|
}
|
||||||
|
const sort = searchParams.sort
|
||||||
|
if (sort) {
|
||||||
|
setSortValues(sortOptions.find((o) => o.value === sort) ?? sortOptions[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
getFetchResult(exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focuses searchbar on load
|
||||||
|
if (!isTouch()) {
|
||||||
|
inputElement?.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableId = "truth-table"
|
||||||
|
const filenameId = "excel-filename"
|
||||||
|
|
||||||
|
function _exportToExcel(): void {
|
||||||
|
const value = getElementById<HTMLInputElement>(filenameId)?.value
|
||||||
|
exportToExcel({
|
||||||
|
name: value !== "" ? value : undefined,
|
||||||
|
tableId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if import.meta.env.DEV}
|
||||||
|
(DEV) Use localhost:
|
||||||
|
<Switch title={"Use localhost"} defaultValue={false} onChange={(state) => useLocalhost = state} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
|
<div class={"mx-auto max-w-2xl"}>
|
||||||
|
<slot />
|
||||||
|
<form class={"flex-row-center"} onSubmit={onFormSubmit} autocomplete={"off"}>
|
||||||
|
<Search ref={inputElement} typingDefault={inputContent} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
id={"truth-input-button"}
|
||||||
|
title={"Generate (Enter)"}
|
||||||
|
type={"submit"}
|
||||||
|
className={"min-w-50px ml-2 h-10"}
|
||||||
|
>
|
||||||
|
Generate
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Options row -->
|
||||||
|
<Row class={"my-1 gap-2"}>
|
||||||
|
<span class={"h-min"}>{"Simplify"}: </span>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
onChange={setSimplifyEnabled}
|
||||||
|
defaultValue={simplifyEnabled()}
|
||||||
|
title={"Simplify"}
|
||||||
|
name={"Turn on/off simplify expressions"}
|
||||||
|
className={"mx-1"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class={"relative h-min"}>
|
||||||
|
<Menu
|
||||||
|
title={"Filter results"}
|
||||||
|
id={"filter-results"}
|
||||||
|
button={
|
||||||
|
<Show
|
||||||
|
when={hideValues().value !== "NONE"}
|
||||||
|
children={
|
||||||
|
<Icon
|
||||||
|
path={eyeSlash}
|
||||||
|
aria-label={"An eye with a slash through it"}
|
||||||
|
class={`mx-1 ${hideValues().value === "TRUE" ? "text-green-500" : "text-red-500"}`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fallback={<Icon path={eye} aria-label={"An eye"} class={"mx-1"} />}
|
||||||
|
keyed
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
children={
|
||||||
|
<For each={hideOptions}>
|
||||||
|
{(option) => (
|
||||||
|
<SingleMenuItem
|
||||||
|
onClick={() => setHideValues(option)}
|
||||||
|
option={option}
|
||||||
|
currentValue={hideValues}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
}
|
||||||
|
itemsClassName={"right-0"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class={"relative h-min"}>
|
||||||
|
<Menu
|
||||||
|
title={"Sort results"}
|
||||||
|
id={"sort-results"}
|
||||||
|
button={
|
||||||
|
<PajamasIcon
|
||||||
|
name="TODO"
|
||||||
|
aria-label={"Filter"}
|
||||||
|
class={`h-6 w-6 ${
|
||||||
|
sortValues().value === "TRUE_FIRST"
|
||||||
|
? "text-green-500"
|
||||||
|
: sortValues().value === "FALSE_FIRST" && "text-red-500"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
children={
|
||||||
|
<For each={sortOptions}>
|
||||||
|
{(option) => (
|
||||||
|
<SingleMenuItem
|
||||||
|
option={option}
|
||||||
|
currentValue={sortValues}
|
||||||
|
onClick={() => setSortValues(option)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
}
|
||||||
|
itemsClassName={"right-0"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
title={"Hide intermediate values"}
|
||||||
|
onChange={setHideIntermediates}
|
||||||
|
defaultValue={hideIntermediates()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Show when={isLoaded() && error() === null} keyed>
|
||||||
|
<MyDialog
|
||||||
|
title={"Download"}
|
||||||
|
description={"Export current table (.xlsx)"}
|
||||||
|
button={
|
||||||
|
<>
|
||||||
|
<p class={"sr-only"}>{"Download"}</p>
|
||||||
|
<Icon aria-label={"Download"} path={arrowDownTray} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
callback={_exportToExcel}
|
||||||
|
acceptButtonName={"Download"}
|
||||||
|
cancelButtonName={"Cancel"}
|
||||||
|
buttonClass={`float-right`}
|
||||||
|
buttonTitle={"Export current table"}
|
||||||
|
acceptButtonId={"download-accept"}
|
||||||
|
>
|
||||||
|
<p>{"Filename"}:</p>
|
||||||
|
<Input
|
||||||
|
className={"border-rounded h-10 px-2"}
|
||||||
|
id={filenameId}
|
||||||
|
placeholder={"Truth Table"}
|
||||||
|
/>
|
||||||
|
</MyDialog>
|
||||||
|
</Show>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Show when={error()} keyed>
|
||||||
|
<ErrorBox
|
||||||
|
title={error()?.title ?? "Error"}
|
||||||
|
error={error()?.message ?? "Something went wrong"}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={isLoaded() === false} keyed>
|
||||||
|
<Icon
|
||||||
|
path={arrowPath}
|
||||||
|
aria-label={"Loading indicator"}
|
||||||
|
class={"mx-auto animate-spin"}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={simplifyEnabled() && (fetchResult()?.orderOperations?.length ?? 0) > 0} keyed>
|
||||||
|
<ShowMeHow fetchResult={fetchResult} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if isLoaded() && error() === null}
|
||||||
|
{#if simplifyEnabled()}
|
||||||
|
<InfoBox
|
||||||
|
className={"mx-auto w-fit pb-1 text-center text-lg"}
|
||||||
|
title={"Output:"}
|
||||||
|
id={"expression-output"}
|
||||||
|
>
|
||||||
|
<p>{fetchResult()?.after}</p>
|
||||||
|
</InfoBox>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class={"m-2 flex justify-center"}>
|
||||||
|
<div id={"table"} class={"h-[45rem] overflow-auto"}>
|
||||||
|
<TruthTable
|
||||||
|
header={fetchResult()?.header ?? undefined}
|
||||||
|
table={fetchResult()?.table?.truthMatrix}
|
||||||
|
id={tableId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
20
src/components/simplifyTruths/SingleMenuItem.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { SimplifyTruthsOption } from "@/types/types.ts"
|
||||||
|
|
||||||
|
export let option: SimplifyTruthsOption
|
||||||
|
export let currentValue: () => SimplifyTruthsOption
|
||||||
|
export let onClick: () => void
|
||||||
|
|
||||||
|
const isSelected = () => option === currentValue()
|
||||||
|
|
||||||
|
// TODO bind on:click
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button class={`flex-row-center cursor-pointer last:mb-1 hover:underline`} on:click={onClick}>
|
||||||
|
<Icon
|
||||||
|
path={check}
|
||||||
|
aria-label={isSelected() ? "A checkmark" : "Nothing"}
|
||||||
|
class={`text-white ${!isSelected() && "invisible"}`}
|
||||||
|
/>
|
||||||
|
{option.name}
|
||||||
|
</button>
|
39
src/components/simplifyTruths/TruthTable.astro
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
import type { Table } from "@/types/types"
|
||||||
|
import type { ComponentProps } from "@/types/props"
|
||||||
|
|
||||||
|
interface Props extends ComponentProps {
|
||||||
|
table?: Table
|
||||||
|
header?: string[]
|
||||||
|
}
|
||||||
|
const { table, header, class: clazz, ...props } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<table class={`z-10 table border-collapse border-2 border-gray-500 ${clazz}`} {...props}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{header?.map(exp => (
|
||||||
|
<th
|
||||||
|
scope={"col"}
|
||||||
|
class={
|
||||||
|
`sticky top-0 bg-default-bg text-center outline
|
||||||
|
outline-2 outline-offset-[-1px] outline-gray-500 [position:-webkit-sticky;]` /*TODO sticky header at the top of the screen */
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p class={"w-max px-2"}>{exp}</p>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{table?.map(row => (
|
||||||
|
<tr class={"hover:text-black"}>
|
||||||
|
{row.map(value => (
|
||||||
|
<td class={`border border-gray-500 text-center last:underline ${value ? "bg-green-700" : "bg-red-700"}`}>
|
||||||
|
<p>{value ? "T" : "F"}</p>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
4
src/components/simplifyTruths/output/Disclosure.astro
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
<div>TODO implement</div>
|
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
<div class={`border-rounded mb-2 flex flex-col gap-1 bg-cyan-900 p-2 dark:border-gray-800 ${Astro.props.className}`}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
12
src/components/simplifyTruths/output/ErrorBox.astro
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
import InfoBox from "./InfoBox.astro"
|
||||||
|
interface Props {
|
||||||
|
title: string
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
const { title, error } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<InfoBox class={"mx-auto w-fit text-center"} title={title} error={true}>
|
||||||
|
<p>{error}</p>
|
||||||
|
</InfoBox>
|
16
src/components/simplifyTruths/output/InfoBox.astro
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
import type { ComponentProps } from "@/types/props"
|
||||||
|
interface Props extends ComponentProps {
|
||||||
|
title: string
|
||||||
|
error?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, error = false, class: clazz, ...props } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class={`border-rounded ${error ? "border-red-500" : "border-gray-500"} ${clazz}`} {...props}>
|
||||||
|
<p class={`border-b px-2 ${error ? "border-red-500" : "border-gray-500"}`}>{title}</p>
|
||||||
|
<div class="mx-2">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
11
src/constants.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export const DOMAIN = "martials.no"
|
||||||
|
export const LINKED_IN_URL = "https://www.linkedin.com/in/martin-b-2a69391a3"
|
||||||
|
export const GIT_BASE_URL = `https://git.${DOMAIN}`
|
||||||
|
export const GIT_PROFILE_URL = `${GIT_BASE_URL}/martials`
|
||||||
|
export const GITHUB_PROFILE_URL = "https://github.com/emberal"
|
||||||
|
export const MASTODON_URL = "https://snabelen.no/@Martials"
|
||||||
|
export const PIXELFED_URL = "https://pixelfed.social/i/web/profile/261454857934868480"
|
||||||
|
export const STEAM_URL = "https://steamcommunity.com/id/martials/"
|
||||||
|
export const THIS_GIT_URL = `${GIT_BASE_URL}/martials/martials.no`
|
||||||
|
export const TRAKT_URL = "https://trakt.tv/users/martials"
|
||||||
|
export const STATUS_URL = `https://status.${DOMAIN}/status/home`
|
@ -5,29 +5,27 @@ 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(),
|
||||||
source: z.string().url(),
|
createdAt: z.string(),
|
||||||
createdAt: z.string().date(),
|
updatedAt: z.string()
|
||||||
updatedAt: z.string().date(),
|
})
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const usesCollection = defineCollection({
|
const hardwareCollection = defineCollection({
|
||||||
loader: glob({ pattern: "**\/*.yaml", base: "./src/content/uses" }),
|
loader: glob({ pattern: "**\/*.yaml", base: "./src/content/hardware" }),
|
||||||
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())),
|
||||||
hardware: z.array(z.string()),
|
hardware: z.array(z.string())
|
||||||
}),
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export const collections = {
|
export const collections = {
|
||||||
projects: projectCollection,
|
projects: projectCollection,
|
||||||
uses: usesCollection,
|
hardware: hardwareCollection
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@ title: Desktop
|
|||||||
accessories:
|
accessories:
|
||||||
- Gaming chair | Arozzi Mezzo V2 Gaming chair Fabric Black/Red # https://www.komplett.no/product/1079732?noredirect=true
|
- 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
|
- Headset | Logitech PRO X LIGHTSPEED Wireless Gaming Headset # https://www.komplett.no/product/1162749?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
|
- Keyboard | Logitech G710
|
||||||
- Monitor 1 | Philips 34" 34M2C6500/00 # https://www.komplett.no/product/1307753/gaming/gaming-utstyr/gamingskjermer/philips-34-gamingskjerm-34m2c650000
|
- Monitor 1 | AOC 27" LED FreeSync G2790PX # https://www.komplett.no/product/975642?noredirect=true
|
||||||
- Monitor 2 | Asus 28" 4K LED PB287Q # https://www.komplett.no/product/815114?noredirect=true
|
- Monitor 2 | Asus 28" 4K LED PB287Q # https://www.komplett.no/product/815114?noredirect=true
|
||||||
- Mouse | Logitech G502 HERO Gaming Mouse
|
- Mouse | Logitech G502 HERO Gaming Mouse
|
||||||
- Mousepad | Svive Styx ESGR Gaming Mousepad XXL # https://www.komplett.no/product/985884?noredirect=true
|
- Mousepad | Svive Styx ESGR Gaming Mousepad XXL # https://www.komplett.no/product/985884?noredirect=true
|
3
src/content/hardware/homeServer.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
title: Home Server
|
||||||
|
hardware:
|
||||||
|
- b # Graphics cards, CPUs, etc.
|
5
src/content/hardware/raspberryPi.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
title: Raspberry Pi 4
|
||||||
|
accessories:
|
||||||
|
- a # Screens, keyboards, mice, etc.
|
||||||
|
hardware:
|
||||||
|
- b # Graphics cards, CPUs, etc.
|
BIN
src/content/projects/Calendar before and after.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
src/content/projects/Simplify-truths-website.png
Normal file
After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 27 KiB |
@ -1,19 +1,14 @@
|
|||||||
---
|
---
|
||||||
lang: "en"
|
title: "Hjemmeside"
|
||||||
title: "Welcome"
|
description: "Hjemmesiden"
|
||||||
description: "Welcome to my homepage / portfolio"
|
heroImage: "./kevin-james.jpg"
|
||||||
heroImage: "assets/recursive-meme.png"
|
heroImageAlt: "The homepage of this site"
|
||||||
heroImageAlt: "A recursive meme that says: Self-reference, recursive meme is self-referential"
|
tags: [Astro, Svelte, TypeScript, I18n]
|
||||||
tags: [Astro, Svelte, TypeScript, I18n, TailwindCSS, Docker]
|
source: "https://example.com"
|
||||||
keywords: [Martin Berg Alstad, portfolio, homepage, website, martials, emberal]
|
|
||||||
source: "https://git.martials.no/martials/martials.no"
|
|
||||||
createdAt: "2024-09-22"
|
createdAt: "2024-09-22"
|
||||||
updatedAt: "2025-02-15"
|
updatedAt: "2024-09-22"
|
||||||
---
|
---
|
||||||
|
|
||||||
This is a short meta post about my homepage which you are looking at right now.
|
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.
|
||||||
This page is going to be a simple site where I will share my projects, stuff i've accomplished and things about me.
|
The site is not built with GatsbyJS and Contentful.
|
||||||
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).
|
|
||||||
|
16
src/content/projects/hvl-ics-simplifier.mdx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
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.
|
BIN
src/content/projects/kevin-james.jpg
Normal file
After Width: | Height: | Size: 364 KiB |
@ -1,115 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
26
src/content/projects/simplify-truths.mdx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
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)
|
@ -1,3 +0,0 @@
|
|||||||
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
|
|
@ -1,5 +0,0 @@
|
|||||||
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
|
|
Before Width: | Height: | Size: 135 KiB |
@ -1,44 +1,31 @@
|
|||||||
---
|
---
|
||||||
|
import Navbar from "@/components/Navbar.astro"
|
||||||
import Footer from "@/components/Footer.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"
|
import { languageTag } from "@/paraglide/runtime"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string
|
title: string
|
||||||
description?: string
|
|
||||||
keywords?: ReadonlyArray<string>
|
|
||||||
class?: string
|
class?: string
|
||||||
}
|
}
|
||||||
const { title, description, keywords, class: clazz } = Astro.props
|
const { title, class: clazz } = Astro.props
|
||||||
const mainClass =
|
|
||||||
"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={languageTag()} dir={"ltr"}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="author" content="Martin Berg Alstad" />
|
<meta name="description" content="Astro description" />
|
||||||
<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/svg+xml" href="/favicon.svg" />
|
||||||
<title>{title} | Martin Berg Alstad</title>
|
<meta name="generator" content={Astro.generator} />
|
||||||
</head>
|
<title>{title}</title>
|
||||||
|
</head>
|
||||||
<body class="flex flex-col min-h-screen bg-cat-base text-cat-text">
|
<body class="flex flex-col h-screen">
|
||||||
<Header />
|
<Navbar />
|
||||||
<main class:list={[mainClass, clazz]}>
|
<main class:list={["grow", clazz]}>
|
||||||
<h1 class="text-center not-sm:hidden">
|
|
||||||
<Breadcrumb />
|
|
||||||
</h1>
|
|
||||||
<div class="my-5">
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</main>
|
||||||
</main>
|
<Footer />
|
||||||
<Footer />
|
</body>
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
14
src/links.ts
@ -9,20 +9,24 @@ interface Link {
|
|||||||
const Links: Link[] = [
|
const Links: Link[] = [
|
||||||
{
|
{
|
||||||
label: m.home,
|
label: m.home,
|
||||||
to: "/",
|
to: "/"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: m.myProjects,
|
label: m.myProjects,
|
||||||
to: "/projects",
|
to: "/projects"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: m.myLinks,
|
label: m.myLinks,
|
||||||
to: "/links",
|
to: "/links"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: m.hardware,
|
||||||
|
to: "/hardware"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: m.contactMe,
|
label: m.contactMe,
|
||||||
to: "/contact",
|
to: "/contact"
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export default Links
|
export default Links
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
import Layout from "../layouts/Layout.astro"
|
import Layout from "../layouts/Layout.astro"
|
||||||
import "@/styles/global.css"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="404">
|
<Layout title="404">
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
import ContactMeForm from "@/components/ContactMeForm.astro"
|
import ContactMePage from "@/components/contactMe/ContactMePage.astro"
|
||||||
import Layout from "@/layouts/Layout.astro"
|
import Layout from "@/layouts/Layout.astro"
|
||||||
import "@/styles/global.css"
|
import "@/styles/global.css"
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Kontakt meg">
|
<Layout title="Kontakt meg">
|
||||||
<ContactMeForm />
|
<ContactMePage />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
import ContactMeForm from "@/components/ContactMeForm.astro"
|
import ContactMePage from "@/components/contactMe/ContactMePage.astro"
|
||||||
import Layout from "@/layouts/Layout.astro"
|
import Layout from "@/layouts/Layout.astro"
|
||||||
import "@/styles/global.css"
|
import "@/styles/global.css"
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Kontakt meg">
|
<Layout title="Kontakt meg">
|
||||||
<ContactMeForm />
|
<ContactMePage />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/layouts/Layout.astro"
|
import Layout from "@/layouts/Layout.astro"
|
||||||
import UsesPage from "@/components/UsesPage.astro"
|
import HardwarePage from "@/components/HardwarePage.svelte"
|
||||||
import "@/styles/global.css"
|
import "@/styles/global.css"
|
||||||
import { getCollection } from "astro:content"
|
import { getCollection } from "astro:content"
|
||||||
|
|
||||||
const uses = await getCollection("uses")
|
const hardware = await getCollection("hardware")
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Hardware" class="mx-auto max-w-[750px]">
|
<Layout title="Hardware" class="mx-auto max-w-[750px]">
|
||||||
<UsesPage uses={uses} />
|
<HardwarePage server:defer hardware={hardware} />
|
||||||
</Layout>
|
</Layout>
|
@ -1,9 +1,8 @@
|
|||||||
---
|
---
|
||||||
import IndexPage from "@/components/landing/IndexPage.astro"
|
import OnePager from "../../components/Greeting.astro"
|
||||||
import Layout from "@/layouts/Layout.astro"
|
import Layout from "../../layouts/Layout.astro"
|
||||||
import "@/styles/global.css"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Welcome">
|
<Layout title="Welcome">
|
||||||
<IndexPage />
|
<OnePager />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/layouts/Layout.astro"
|
import Layout from "@/layouts/Layout.astro"
|
||||||
import LinksPage from "@/components/myLinks/LinksPage.astro"
|
import LinksPage from "@/components/myLinks/LinksPage.astro"
|
||||||
import "@/styles/global.css"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="My links">
|
<Layout title="My links">
|
||||||
<LinksPage />
|
<LinksPage />
|
||||||
</Layout>
|
</Layout>
|
@ -1,15 +1,15 @@
|
|||||||
---
|
---
|
||||||
import ProjectPage from "@/components/projects/ProjectPage.astro"
|
import ProjectPage from "@/components/projects/ProjectPage.astro"
|
||||||
import { type GetStaticPathsResult } from "astro"
|
import { type GetStaticPathsResult } from "astro"
|
||||||
import "@/styles/global.css"
|
|
||||||
|
|
||||||
// Prerender the page as static HTML during build
|
// Prerender the page as static HTML during build
|
||||||
export const prerender = true
|
export const prerender = true
|
||||||
|
|
||||||
export function getStaticPaths(): GetStaticPathsResult {
|
export function getStaticPaths(): GetStaticPathsResult {
|
||||||
return [
|
return [
|
||||||
|
{ params: { project: "hvl-ics-simplifier" } },
|
||||||
{ params: { project: "homepage" } },
|
{ params: { project: "homepage" } },
|
||||||
{ params: { project: "sb1budget" } },
|
{ params: { project: "simplify-truths" } }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
---
|
---
|
||||||
|
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"
|
|
||||||
|
const projects = await getCollection("projects")
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Projects">
|
<Layout title="Projects">
|
||||||
<MyProjectsPage />
|
<MyProjectsPage projects={projects} />
|
||||||
</Layout>
|
</Layout>
|
8
src/pages/en/simplify-truths.astro
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
import Layout from "@/layouts/Layout.astro"
|
||||||
|
import SimplifyTruthsPage from "@/components/simplifyTruths/SimplifyTruthsPage.astro"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="Simplify truths">
|
||||||
|
<SimplifyTruthsPage />
|
||||||
|
</Layout>
|
@ -1,12 +1,12 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/layouts/Layout.astro"
|
import Layout from "@/layouts/Layout.astro"
|
||||||
import UsesPage from "@/components/UsesPage.astro"
|
import HardwarePage from "@/components/HardwarePage.svelte"
|
||||||
import "@/styles/global.css"
|
import "@/styles/global.css"
|
||||||
import { getCollection } from "astro:content"
|
import { getCollection } from "astro:content"
|
||||||
|
|
||||||
const uses = await getCollection("uses")
|
const hardware = await getCollection("hardware")
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Hardware" class="mx-auto max-w-[750px]">
|
<Layout title="Hardware" class="mx-auto max-w-[750px]">
|
||||||
<UsesPage uses={uses} />
|
<HardwarePage server:defer hardware={hardware} />
|
||||||
</Layout>
|
</Layout>
|
@ -1,9 +1,8 @@
|
|||||||
---
|
---
|
||||||
import IndexPage from "@/components/landing/IndexPage.astro"
|
import Layout from "../layouts/Layout.astro"
|
||||||
import Layout from "@/layouts/Layout.astro"
|
import Greeting from "../components/Greeting.astro"
|
||||||
import "@/styles/global.css"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Velkommen">
|
<Layout title="Velkommen">
|
||||||
<IndexPage />
|
<Greeting />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/layouts/Layout.astro"
|
import Layout from "@/layouts/Layout.astro"
|
||||||
import LinksPage from "@/components/myLinks/LinksPage.astro"
|
import LinksPage from "@/components/myLinks/LinksPage.astro"
|
||||||
import "@/styles/global.css"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Mine lenker">
|
<Layout title="Mine lenker">
|
||||||
<LinksPage />
|
<LinksPage />
|
||||||
</Layout>
|
</Layout>
|
@ -1,15 +1,15 @@
|
|||||||
---
|
---
|
||||||
import ProjectPage from "@/components/projects/ProjectPage.astro"
|
import ProjectPage from "@/components/projects/ProjectPage.astro"
|
||||||
import { type GetStaticPathsResult } from "astro"
|
import { type GetStaticPathsResult } from "astro"
|
||||||
import "@/styles/global.css"
|
|
||||||
|
|
||||||
// Prerender the page as static HTML during build
|
// Prerender the page as static HTML during build
|
||||||
export const prerender = true
|
export const prerender = true
|
||||||
|
|
||||||
export function getStaticPaths(): GetStaticPathsResult {
|
export function getStaticPaths(): GetStaticPathsResult {
|
||||||
return [
|
return [
|
||||||
|
{ params: { project: "hvl-ics-simplifier" } },
|
||||||
{ params: { project: "homepage" } },
|
{ params: { project: "homepage" } },
|
||||||
{ params: { project: "sb1budget" } },
|
{ params: { project: "simplify-truths" } }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
---
|
---
|
||||||
|
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"
|
|
||||||
|
const projects = await getCollection("projects")
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Prosjekter">
|
<Layout title="Prosjekter">
|
||||||
<MyProjectsPage />
|
<MyProjectsPage projects={projects} />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
8
src/pages/simplify-truths.astro
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
import Layout from "@/layouts/Layout.astro"
|
||||||
|
import SimplifyTruthsPage from "@/components/simplifyTruths/SimplifyTruthsPage.astro"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="Forenkle sannhetsverdier">
|
||||||
|
<SimplifyTruthsPage />
|
||||||
|
</Layout>
|
@ -1,28 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
@plugin "@tailwindcss/typography";
|
@tailwind utilities;
|
||||||
@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 {
|
@layer utilities {
|
||||||
.debug {
|
.debug {
|
||||||
@ -30,24 +8,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
br {
|
@layer base {
|
||||||
|
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 {
|
/* TODO change default style*/
|
||||||
@apply text-2xl font-bold mb-2;
|
a {
|
||||||
}
|
@apply link
|
||||||
|
}
|
||||||
/* TODO change default style*/
|
|
||||||
a {
|
|
||||||
@apply link text-cat-mauve;
|
|
||||||
text-decoration-line: none;
|
|
||||||
}
|
}
|
||||||
|