Compare commits

9 Commits

Author SHA1 Message Date
b6a332a2fe moved files from old repo and rewrote some 2024-12-17 16:58:58 +00:00
4724b0a0e0 Moved some contstants to a constants.ts file.
Added LinkedIn link in footer.

Added option to add icon to ExternalLinks

Refactored some code in ExternalLink.

Signed-off-by: Martin Berg Alstad <git@martials.no>
2024-10-22 20:46:53 +02:00
b9f7b63aa9 Fix localization on detailed project pages.
All checks were successful
Build and deploy website / build (push) Successful in 34s
Fix way too much padding on <br/> tags.

Added some old project entries.

Added Project as a convenience type.

Added link class to all a tags to easier distinguish links in text.

Signed-off-by: Martin Berg Alstad <git@martials.no>
2024-10-20 22:25:52 +02:00
941a93f8a5 Updated to Astro v5 beta.
All checks were successful
Build and deploy website / build (push) Successful in 1m56s
Created a new component for a collapsable list

Implemented some of the new features.
- astro:env
- New astro content layer

Signed-off-by: Martin Berg Alstad <git@martials.no>
2024-10-20 12:05:32 +02:00
32f4c6aaf0 Update container name and stricter types for linking
Signed-off-by: Martin Berg Alstad <git@martials.no>
2024-10-20 11:01:54 +02:00
b8e77b2a54 Docker and Docker compose
All checks were successful
Build and deploy website / build (push) Successful in 3m26s
Updated dependencies

Updated workflow to use docker compose

Signed-off-by: Martin Berg Alstad <git@martials.no>
2024-10-12 19:07:55 +02:00
740cba625d SSR and i18n
Signed-off-by: Martin Berg Alstad <git@martials.no>
2024-10-12 18:29:44 +02:00
1a2fec6a59 security.txt
Signed-off-by: Martin Berg Alstad <git@martials.no>
2024-10-12 17:12:12 +02:00
c701a510f7 Button group to switch languages.
Localized pathname function for links.

inlang/paraglide-astro package

Signed-off-by: Martin Berg Alstad <git@martials.no>
2024-10-09 21:30:05 +02:00
79 changed files with 3350 additions and 1168 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
.astro
.gitea
.DS_Store
node_modules
dist

2
.env
View File

@ -1,2 +0,0 @@
GIT_URL="https://git.martials.no"
STATUS_URL="https://status.martials.no/status/home"

View File

@ -1,40 +0,0 @@
name: Build and deploy website
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Install dependencies
run: echo y | npm exec -- pnpm install
- name: Build
run: npm exec -- pnpm build
- name: Upload artifacts
uses: actions/upload-artifact@v3 # Deprecated and v4+ is not supported for GHES
with:
name: dist
path: dist
deploy:
runs-on: host
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: dist
path: dist
- name: Move files to server
run: |
rm -rf /var/www/beta.martials.no/*
cp -r dist/* /var/www/beta.martials.no

View File

@ -0,0 +1,19 @@
name: Build and deploy website
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
runs-on: host
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Run docker-compose
run: docker compose up -d --build

26
Dockerfile Normal file
View File

@ -0,0 +1,26 @@
FROM node:lts-alpine AS base
WORKDIR /app
# By copying only the package.json and package-lock.json here, we ensure that the following `-deps` steps are independent of the source code.
# Therefore, the `-deps` steps will be skipped if only the source code changes.
COPY package.json pnpm-lock.yaml ./
COPY project.inlang ./project.inlang
FROM base AS prod-deps
RUN echo y | npm exec -- pnpm install --prod
FROM base AS build-deps
RUN npm exec -- pnpm install
FROM build-deps AS build
COPY . .
RUN npm exec -- pnpm run build
FROM base AS runtime
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD node ./dist/server/entry.mjs

View File

@ -1,13 +1,15 @@
// @ts-check
import { defineConfig } from "astro/config"
import { defineConfig, envField } from "astro/config"
import paraglide from "@inlang/paraglide-astro"
import tailwind from "@astrojs/tailwind"
import sitemap from "@astrojs/sitemap"
import { loadEnv } from "vite"
import mdx from "@astrojs/mdx"
import svelte from "@astrojs/svelte"
import node from "@astrojs/node"
import mdx from "@astrojs/mdx"
import icon from "astro-icon"
import { loadEnv } from "vite"
const { url } = process.env.URL
? loadEnv(process.env.URL, process.cwd(), "")
: { url: "http://localhost:3000" }
@ -15,10 +17,29 @@ const { url } = process.env.URL
// https://astro.build/config
export default defineConfig({
site: url,
// output: "server", TODO server | also required for i18n
output: "server",
i18n: {
defaultLocale: "nb",
locales: ["nb", "en"]
},
integrations: [tailwind(), sitemap(), mdx(), svelte(), icon()]
integrations: [
tailwind(),
sitemap(),
mdx(),
svelte(),
icon(),
paraglide({
// recommended settings
project: "./project.inlang",
outdir: "./src/paraglide" //where your files should be
})
],
adapter: node({
mode: "standalone"
}),
env: {
schema: {
URL: envField.string({ context: "client", access: "public" })
}
}
})

9
docker-compose.yml Normal file
View File

@ -0,0 +1,9 @@
services:
web:
container_name: martials.no
restart: always
build:
context: .
dockerfile: Dockerfile
ports:
- "4321:4321"

View File

@ -18,5 +18,8 @@
"subject": "Subject",
"email": "Email",
"message": "Message",
"send": "Send"
"send": "Send",
"auto": "Auto",
"norwegian": "Norwegian",
"english": "English"
}

View File

@ -18,5 +18,8 @@
"subject": "Emne",
"email": "E-post",
"message": "Melding",
"send": "Send"
"send": "Send",
"auto": "Auto",
"norwegian": "Norsk",
"english": "Engelsk"
}

View File

@ -10,26 +10,30 @@
"astro": "astro",
"postinstall": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide",
"format": "prettier --write \"./src/**/*.{js,mjs,ts,astro,svelte,css,md,json}\"",
"watch-translations": "paraglide-js compile --watch --project ./project.inlang --outdir ./src/paraglide"
"watch-messages": "paraglide-js compile --watch --project ./project.inlang --outdir ./src/paraglide"
},
"dependencies": {
"@astrojs/check": "^0.9.3",
"@astrojs/mdx": "^3.1.7",
"@astrojs/sitemap": "^3.1.6",
"@astrojs/svelte": "^5.7.1",
"@astrojs/tailwind": "^5.1.1",
"@iconify-json/pajamas": "^1.2.2",
"@astrojs/check": "^0.9.4",
"@astrojs/mdx": "4.0.0-beta.2",
"@astrojs/node": "9.0.0-beta.2",
"@astrojs/sitemap": "^3.2.1",
"@astrojs/svelte": "6.0.0-beta.1",
"@astrojs/tailwind": "^5.1.2",
"@iconify-json/pajamas": "^1.2.3",
"@inlang/paraglide-astro": "^0.2.2",
"@inlang/paraglide-js": "1.11.2",
"@tailwindcss/typography": "^0.5.15",
"astro": "^4.15.9",
"astro": "5.0.0-beta.5",
"astro-icon": "^1.1.1",
"diff": "^7.0.0",
"sharp": "^0.33.5",
"svelte": "^4.2.19",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.2"
"typescript": "^5.6.3"
},
"devDependencies": {
"@inlang/paraglide-js": "1.11.2",
"daisyui": "^4.12.10",
"@types/diff": "^5.2.3",
"daisyui": "^4.12.13",
"prettier": "^3.3.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-svelte": "^3.2.7",

2709
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
Contact: mailto:security@martials.no
Expires: 2029-12-31T23:00:00.000Z
Preferred-Languages: no,en
Canonical: https://martials.no/.well-known/security.txt

View File

@ -1,18 +1,21 @@
---
import GiteaLink from "./links/GiteaLink.astro"
import ExternalLink from "./links/ExternalLink.astro"
import PajamasIcon from "./icons/PajamasIcon.astro"
import LanguageButtonGroup from "./LanguageButtonGroup.astro"
import * as m from "@/paraglide/messages"
const gitUrl = import.meta.env.GIT_URL
const statusUrl = import.meta.env.STATUS_URL
import { LINKED_IN_URL, THIS_GIT_URL, STATUS_URL } from "../constants"
---
<div class="divider"></div>
<div class="mx-auto py-5 flex flex-col gap-1 items-center">
<GiteaLink href={`${gitUrl}/martials/martials.no`} />
<ExternalLink href={statusUrl} class="flex items-center" title="Status">
<PajamasIcon name="pajamas:status-health" class="w-6 h-6 mr-2" />
{m.status()}
</ExternalLink>
<div class="divider" />
<div class="py-5 flex flex-row gap-1 justify-around w-full items-center">
<div class="flex flex-col gap-1">
<GiteaLink href={THIS_GIT_URL} />
<ExternalLink href={LINKED_IN_URL} iconLeft="pajamas:linkedin" iconLeftAriaLabel="LinkedIn" title="LinkedIn">
LinkedIn
</ExternalLink>
<ExternalLink href={STATUS_URL} iconLeft="pajamas:status-health" iconLeftAriaLabel="Status health" title="Status">
{m.status()}
</ExternalLink>
</div>
<LanguageButtonGroup />
</div>

View File

@ -1,9 +1,10 @@
<script lang="ts">
import Select from "./Select.svelte"
import * as m from "@/paraglide/messages"
import Collapse from "@/components/Collapse.svelte"
import CollapseList from "@/components/collapse/CollapseList.svelte"
import type { CollectionEntry } from "astro:content"
export let hardware: any[] = []
export let hardware: CollectionEntry<"hardware">[] = []
const hardwareOptions = hardware.map((item) => ({
key: item.id,
@ -13,6 +14,7 @@
let selectedHardwareKey: string = hardware[0].id
$: selectedHardware = hardware.find((item) => item.id === selectedHardwareKey)!
// TODO bind to component
function onChange({ detail }: CustomEvent<string>) {
selectedHardwareKey = detail
}
@ -25,20 +27,9 @@
</div>
<br />
<Collapse title={m.hardware()}>
<ul>
{#each selectedHardware.data.hardware as item}
<li class="list-disc ml-5">{item}</li>
{/each}
</ul>
</Collapse>
<CollapseList items={selectedHardware.data.hardware} title={m.hardware()} />
<div class="my-2" />
{#if (selectedHardware.data.accessories)}
<Collapse title={m.accessories()}>
<ul>
{#each selectedHardware.data.accessories as item}
<li class="list-disc ml-5">{item}</li>
{/each}
</ul>
</Collapse>
<CollapseList items={selectedHardware.data.accessories} title={m.accessories()} />
{/if}
</div>

View File

@ -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">
{label}
<input
class="input input-bordered"
type={type}
name={name}
required={required}
placeholder={placeholder}
/>
</label>

View 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>

View File

@ -0,0 +1,11 @@
---
import LocaleLink from "./links/LocaleLink.astro"
import { type NavLink, resolvePathname } from "@/utils/linking"
const currentPath = resolvePathname(Astro.url.pathname)
---
<div class="join">
<LocaleLink to={currentPath as NavLink} lang="nb" class="btn join-item">Norsk</LocaleLink>
<LocaleLink to={currentPath as NavLink} lang="en" class="btn join-item">English</LocaleLink>
</div>

View File

@ -0,0 +1,7 @@
<script lang="ts">
</script>
<div>
<!-- TODO -->
</div>

View File

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

View File

@ -0,0 +1,7 @@
<script lang="ts">
</script>
<div class="flex flex-row items-center ${$$restProps.class}">
<slot />
</div>

View 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>

View 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>

View File

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

View File

@ -1,5 +1,5 @@
---
import Input from "@/components/Input.astro"
import Input from "@/components/Input.svelte"
import * as m from "@/paraglide/messages.js"
// TODO self-host email server
---

View File

@ -0,0 +1,7 @@
---
import ContactMeForm from "./ContactMeForm.astro"
import * as m from "@/paraglide/messages"
---
<h1 class="text-center">{m.contactMe()}</h1>
<ContactMeForm />

View File

@ -7,4 +7,4 @@ interface Props {
const { class: clazz } = Astro.props
---
<PajamasIcon name="pajamas:gitea" class={clazz}></PajamasIcon>
<PajamasIcon name="pajamas:gitea" aria-label="Gitea" class={clazz} />

View File

@ -2,11 +2,13 @@
import type { PajamasIcon } from "@/types/icons"
import type { ComponentProps } from "@/types/props"
import { Icon } from "astro-icon/components"
interface Props extends ComponentProps {
name: PajamasIcon
"aria-label": string
}
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} />

View 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} />

View File

@ -1,12 +1,28 @@
---
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 {
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 href={href} target="_blank" rel="noopener" class:list={[noStyle ? "" : "link", clazz]} {...props}>
{ iconLeft && iconLeftAriaLabel
?
<ExternalLinkIconLeft iconLeft={iconLeft} iconLeftAriaLabel={iconLeftAriaLabel} {...props}>
<slot />
</a>
</ExternalLinkIconLeft>
:
<ExternalLinkTextOnly {...props}>
<slot />
</ExternalLinkTextOnly>
}

View 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>

View 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>

View File

@ -1,7 +1,6 @@
---
import ExternalLink from "./ExternalLink.astro"
import * as m from "@/paraglide/messages"
import Gitea from "../icons/Gitea.astro"
interface Props {
href: string
}
@ -9,8 +8,7 @@ const { href } = Astro.props
---
<div>
<ExternalLink href={href} class="flex items-center gap-1">
<Gitea class="w-6 h-6" />
<ExternalLink iconLeft="pajamas:gitea" iconLeftAriaLabel="Gitea" href={href}>
{m.sourceCode()}
</ExternalLink>
</div>

View File

@ -0,0 +1,16 @@
---
import { languageTag, type AvailableLanguageTag } from "@/paraglide/runtime"
import { localizePathname, type NavLink } from "@/utils/linking"
import type { ComponentProps } from "@/types/props"
interface Props extends ComponentProps {
to: NavLink
lang?: AvailableLanguageTag
}
const { to, class: clazz, lang = languageTag() } = Astro.props
---
<a href={localizePathname(to, lang)} class={clazz}>
<slot />
</a>

View File

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

View File

@ -1,7 +1,6 @@
import type { PajamasIcon } from "@/types/icons.ts"
import * as m from "@/paraglide/messages"
const gitUrl = import.meta.env.GIT_URL
import * as c from "@/constants.ts"
export interface MyLink {
title: string;
@ -13,35 +12,35 @@ export interface MyLink {
export default [
{
title: "GitHub",
url: "https://github.com/emberal",
url: c.GITHUB_PROFILE_URL,
icon: "pajamas:github"
},
{
title: "Gitea",
url: `${gitUrl}/martials`,
url: c.GIT_PROFILE_URL,
message: m.forPersonalProjects(),
icon: "pajamas:gitea"
},
{
title: "LinkedIn",
url: "https://www.linkedin.com/in/martin-b-2a69391a3/",
url: c.LINKED_IN_URL,
icon: "pajamas:linkedin"
},
{
title: "Mastodon (Snabelen)",
url: "https://snabelen.no/@Martials",
url: c.MASTODON_URL,
icon: "pajamas:mastodon"
},
{
title: "Pixelfed",
url: "https://pixelfed.social/i/web/profile/261454857934868480"
url: c.PIXELFED_URL
},
{
title: "Steam",
url: "https://steamcommunity.com/id/martials/"
url: c.STEAM_URL
},
{
title: "Trakt.tv",
url: "https://trakt.tv/users/martials"
url: c.TRAKT_URL
}
] satisfies MyLink[]

View File

@ -1,12 +1,16 @@
---
import ProjectCard from "./ProjectCard.astro"
import * as m from "@/paraglide/messages"
import { type CollectionEntry } from "astro:content"
import { type NavLink } from "@/utils/linking"
interface Props {
projects: any[] // TODO Type this
projects: CollectionEntry<"projects">[]
}
const { projects } = Astro.props
const baseUrl: NavLink = "/projects"
---
<h1 class="text-4xl font-bold text-center sm:my-10 mt-2">{m.myProjects()}</h1>
@ -15,12 +19,12 @@ const { projects } = Astro.props
projects.map(
({
data: { title, description, tags, heroImage, heroImageAlt },
slug
id
}) => (
<div class="my-5 px-2">
<ProjectCard
title={title}
linkTo={`/project/${slug}`}
linkTo={`${baseUrl}/${id}`}
description={description}
tags={tags}
image={heroImage}

View File

@ -2,6 +2,8 @@
import { Image } from "astro:assets"
import { type ImageMetadata } from "astro"
import BadgeList from "../badge/BadgeList.astro"
import LocaleLink from "../links/LocaleLink.astro"
import { type NavLink } from "@/utils/linking"
interface Props {
title: string
@ -9,14 +11,14 @@ interface Props {
tags: string[]
image: ImageMetadata
imageAlt: string
linkTo: string
linkTo: NavLink
}
const { title, description, tags, image, imageAlt, linkTo } = Astro.props
---
<a
href={linkTo}
<LocaleLink
to={linkTo}
class="card bg-base-100 max-w-96 shadow-xl hover:scale-105 transition"
>
<figure>
@ -29,4 +31,4 @@ const { title, description, tags, image, imageAlt, linkTo } = Astro.props
<p>{description}</p>
<BadgeList tags={tags} />
</div>
</a>
</LocaleLink>

View File

@ -1,7 +1,7 @@
---
import Layout from "@/layouts/Layout.astro"
import { Image } from "astro:assets"
import { getEntry } from "astro:content"
import { getEntry, render } from "astro:content"
import BadgeList from "../badge/BadgeList.astro"
import * as m from "@/paraglide/messages"
import { languageTag } from "@/paraglide/runtime"
@ -15,7 +15,7 @@ interface Props {
const { project } = Astro.props
const entry = await getEntry("projects", project)
const { Content } = await entry!.render()
const { Content } = await render(entry)
const {
title,
description,

View 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>

View 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>

View 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>
}
/>

View 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>

View File

@ -0,0 +1,10 @@
---
import SimplifyTruthsPageBody from "./SimplifyTruthsPageBody.svelte"
import HowTo from "./HowTo.astro"
---
<div id={"truth-content"}>
<SimplifyTruthsPageBody>
<HowTo />
</SimplifyTruthsPageBody>
</div>

View 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>
)

View 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}

View 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>

View 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>

View File

@ -0,0 +1,4 @@
---
---
<div>TODO implement</div>

View File

@ -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>

View 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>

View 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
View 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`

View File

@ -1,7 +1,8 @@
import { defineCollection, z } from "astro:content"
import { glob } from "astro/loaders"
const projectCollection = defineCollection({
type: "content",
loader: glob({ pattern: "**\/*.mdx", base: "./src/content/projects" }),
schema: ({ image }) =>
z.object({
title: z.string(),
@ -16,7 +17,7 @@ const projectCollection = defineCollection({
})
const hardwareCollection = defineCollection({
type: "data",
loader: glob({ pattern: "**\/*.yaml", base: "./src/content/hardware" }),
schema: z.object({
title: z.string(),
accessories: z.optional(z.array(z.string())),

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -1,12 +0,0 @@
---
title: "Hotel Service"
description: "REST API for managing hotels"
heroImage: "./kevin-james.jpg"
heroImageAlt: "The homepage of this site"
tags: [Rust, Axum, Postgres, REST]
source: "https://example.com"
createdAt: "2024-09-22"
updatedAt: "2024-09-22"
---
Hello

View 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.

View 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)

12
src/env.d.ts vendored
View File

@ -1,12 +0,0 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
interface ImportMetaEnv {
readonly URL: string
readonly GIT_URL: string
readonly STATUS_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@ -1,23 +1,32 @@
import * as m from "./paraglide/messages.js"
import * as m from "@/paraglide/messages.js"
import type { NavLink } from "@/utils/linking.ts"
interface Link {
label: string
to: string
label: () => string
to: NavLink
}
const Links: Link[] = [
{
label: m.home(),
to: "/",
label: m.home,
to: "/"
},
{
label: m.myProjects(),
to: "/project",
label: m.myProjects,
to: "/projects"
},
{
label: m.contactMe(),
to: "/contact-me",
label: m.myLinks,
to: "/links"
},
{
label: m.hardware,
to: "/hardware"
},
{
label: m.contactMe,
to: "/contact"
}
]
export default Links

View File

@ -0,0 +1,15 @@
import type { APIRoute } from "astro"
function getSecurityTxt(site?: URL) {
const canonical = new URL("/.well-known/security.txt", site)
return `
Contact: mailto:security@martials.no
Expires: 2029-12-31T23:00:00.000Z
Preferred-Languages: no,en
Canonical: ${canonical.href}
`
}
export const GET: APIRoute = ({ site }) => {
return new Response(getSecurityTxt(site))
}

View File

@ -1,9 +1,9 @@
---
import ContactMeForm from "@/components/ContactMeForm.astro"
import ContactMePage from "@/components/contactMe/ContactMePage.astro"
import Layout from "@/layouts/Layout.astro"
import "@/styles/global.css"
---
<Layout title="Kontakt meg">
<ContactMeForm />
<ContactMePage />
</Layout>

View File

@ -1,9 +1,9 @@
---
import ContactMeForm from "@/components/ContactMeForm.astro"
import ContactMePage from "@/components/contactMe/ContactMePage.astro"
import Layout from "@/layouts/Layout.astro"
import "@/styles/global.css"
---
<Layout title="Kontakt meg">
<ContactMeForm />
<ContactMePage />
</Layout>

View File

@ -8,5 +8,5 @@ const hardware = await getCollection("hardware")
---
<Layout title="Hardware" class="mx-auto max-w-[750px]">
<HardwarePage client:load hardware={hardware} />
<HardwarePage server:defer hardware={hardware} />
</Layout>

View File

@ -1,15 +0,0 @@
---
import ProjectPage from "../../../components/projects/ProjectPage.astro"
import { type GetStaticPathsResult } from "astro"
export function getStaticPaths(): GetStaticPathsResult {
return [
{ params: { project: "hotelservice" } },
{ params: { project: "homepage" } }
]
}
const { project } = Astro.params
---
<ProjectPage project={project as string} />

View File

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

View File

@ -0,0 +1,8 @@
---
import Layout from "@/layouts/Layout.astro"
import SimplifyTruthsPage from "@/components/simplifyTruths/SimplifyTruthsPage.astro"
---
<Layout title="Simplify truths">
<SimplifyTruthsPage />
</Layout>

View File

@ -8,5 +8,5 @@ const hardware = await getCollection("hardware")
---
<Layout title="Hardware" class="mx-auto max-w-[750px]">
<HardwarePage client:load hardware={hardware} />
<HardwarePage server:defer hardware={hardware} />
</Layout>

View File

@ -1,15 +0,0 @@
---
import ProjectPage from "../../components/projects/ProjectPage.astro"
import { type GetStaticPathsResult } from "astro"
export function getStaticPaths(): GetStaticPathsResult {
return [
{ params: { project: "hotelservice" } },
{ params: { project: "homepage" } }
]
}
const { project } = Astro.params
---
<ProjectPage project={project as string} />

View File

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

View File

@ -0,0 +1,8 @@
---
import Layout from "@/layouts/Layout.astro"
import SimplifyTruthsPage from "@/components/simplifyTruths/SimplifyTruthsPage.astro"
---
<Layout title="Forenkle sannhetsverdier">
<SimplifyTruthsPage />
</Layout>

View File

@ -10,7 +10,7 @@
@layer base {
br {
@apply my-4;
@apply my-0.5;
}
h1 {
@ -20,4 +20,9 @@
h2 {
@apply text-3xl font-bold mb-2;
}
/* TODO change default style*/
a {
@apply link
}
}

View File

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

42
src/types/types.ts Normal file
View File

@ -0,0 +1,42 @@
import type { CollectionEntry } from "astro:content"
export type AbsolutePathname = `/${string}`
export type Project = CollectionEntry<"projects">
export type Operator = "AND" | "OR" | "NOT" | "IMPLICATION"
export type Table = boolean[][]
export type OrderOfOperation = {
before: string
after: string
law: string
}
export type Expression = {
leading: string
left: Expression | null
operator: Operator | null
right: Expression | null
trailing: string
atomic: string | null
}
export type FetchResult = {
status: string
version: string | null
before: string
after: string
orderOperations: OrderOfOperation[] | null
expression: Expression | null
header: string[] | null
table: {
truthMatrix: Table
} | null
}
export type SimplifyTruthsOption = {
name: string
value: "NONE" | "TRUE" | "FALSE" | "DEFAULT" | "TRUE_FIRST" | "FALSE_FIRST"
}

66
src/utils/linking.ts Normal file
View File

@ -0,0 +1,66 @@
import type { AvailableLanguageTag } from "@/paraglide/runtime.js"
import type { AbsolutePathname, Project } from "@/types/types.ts"
interface TranslatedPathnames {
nb: AbsolutePathname
en: `/en${AbsolutePathname}`
}
export type NavLink =
"/"
| "/contact"
| "/projects"
| `/projects/${Project["id"]}`
| "/links"
| "/hardware"
const paths: Set<NavLink> = new Set([
"/",
"/contact",
"/projects",
"/links",
"/hardware"
])
/**
* Defines the localized pathnames for the site.
* The key must be used to navigate to the correct path.
* The value is the path that will be used for the given locale.
*
* @see https://inlang.com/m/iljlwzfs/paraglide-astro-i18n
*/
const pathnames: Record<AbsolutePathname, TranslatedPathnames> = {}
for (const path of paths) {
pathnames[path] = {
nb: path,
en: `/en${path}`
}
}
export function localizePathname(
pathname: NavLink,
locale: AvailableLanguageTag
): string {
const pathnameParts = pathname.split("/")
const firstSegment: AbsolutePathname = `/${pathnameParts[1]}`
if (pathnames[firstSegment]) {
const localizedPathname = pathnames[firstSegment][locale]
const rest = pathnameParts.slice(2)
if (rest.length > 0) {
return `${localizedPathname}/${rest.join("/")}`
} else {
return localizedPathname
}
}
return pathname
}
export function resolvePathname(pathname: string): AbsolutePathname {
const enPattern = "/en"
if (pathname.startsWith(enPattern)) {
return pathname.slice(enPattern.length) as AbsolutePathname
}
return pathname as AbsolutePathname
}

View File

@ -1,10 +1,14 @@
{
"extends": "astro/tsconfigs/strict",
"include": [
".astro/types.d.ts",
"src/**/*.ts",
"src/**/*.astro",
"src/**/*.svelte"
],
"exclude": [
"dist"
],
"compilerOptions": {
"strictNullChecks": true,
"allowJs": true,