Compare commits
12 Commits
1fa2667deb
...
ddbb5bc09b
Author | SHA1 | Date | |
---|---|---|---|
ddbb5bc09b
|
|||
53a0881051
|
|||
68b752e15f
|
|||
b524091d38
|
|||
29887ab25c
|
|||
1e0ab6b0c8
|
|||
45226136f3
|
|||
a859439353
|
|||
969660abc8
|
|||
99aced7367
|
|||
78b333e9f7
|
|||
1d04befff1
|
2
.env
2
.env
@ -1,3 +1,3 @@
|
||||
DOMAIN="martials.no"
|
||||
GIT_URL=https://git.$DOMAIN
|
||||
GIT_URL=https://code.$DOMAIN
|
||||
STATUS_URL="https://status.$DOMAIN/status/home"
|
@ -4,9 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@ -15,5 +13,9 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
- name: Run docker-compose
|
||||
run: docker compose up -d --build
|
||||
- name: Build production
|
||||
run: docker compose up -d --build prod
|
||||
if: gitea.ref == 'refs/heads/master'
|
||||
- name: Build develop
|
||||
run: docker compose up -d --build dev
|
||||
if: gitea.ref == 'refs/heads/develop'
|
7
TODO.md
7
TODO.md
@ -3,11 +3,16 @@
|
||||
- [ ] License
|
||||
|
||||
## Code
|
||||
- [ ] Nix Shell
|
||||
- [x] Nix Shell
|
||||
- [ ] Analytics
|
||||
- [ ] Organize code better
|
||||
- [ ] Type slug of project
|
||||
|
||||
## CI/CD
|
||||
- [ ] Staging environment
|
||||
- [ ] Deploy to staging environment on push to master
|
||||
- [ ] Deploy to production environment on push tag to master
|
||||
|
||||
## SEO
|
||||
- [ ] Meta tags on each page
|
||||
|
||||
|
57
biome.jsonc
Normal file
57
biome.jsonc
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": ["./src/paraglide"],
|
||||
"include": ["./src/**"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space"
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double",
|
||||
"semicolons": "asNeeded",
|
||||
"arrowParentheses": "asNeeded"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["*.astro"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useConst": "off",
|
||||
"useImportType": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["*.svelte"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useConst": "off",
|
||||
"useImportType": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -1,9 +1,20 @@
|
||||
services:
|
||||
web:
|
||||
container_name: martials.no
|
||||
prod:
|
||||
hostname: martials.no
|
||||
container_name: "martials.no"
|
||||
restart: always
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "4321:4321"
|
||||
- "4321:4321"
|
||||
|
||||
dev:
|
||||
hostname: dev.martials.no
|
||||
container_name: "dev.martials.no"
|
||||
restart: always
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "4322:4321"
|
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1751211869,
|
||||
"narHash": "sha256-1Cu92i1KSPbhPCKxoiVG5qnoRiKTgR5CcGSRyLpOd7Y=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b43c397f6c213918d6cfe6e3550abfe79b5d1c51",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-25.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
37
flake.nix
Normal file
37
flake.nix
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
description = "martials.no Development environment";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
|
||||
};
|
||||
|
||||
outputs = { nixpkgs, ... }:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
in
|
||||
{
|
||||
devShells.${system}.default =
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
in
|
||||
pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
git
|
||||
] ++ [
|
||||
# Node
|
||||
nodejs_22
|
||||
pnpm
|
||||
] ++ [
|
||||
nodePackages.prettier
|
||||
biome
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
pnpm install
|
||||
fish
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
53
package.json
53
package.json
@ -10,49 +10,32 @@
|
||||
"astro": "astro",
|
||||
"type-check": "astro check",
|
||||
"postinstall": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide",
|
||||
"format": "prettier --write \"./src/**/*.{js,mjs,ts,astro,svelte,css,md,json}\"",
|
||||
"format": "biome format --write .",
|
||||
"lint": "biome lint --write .",
|
||||
"lint:fix": "biome check --write .",
|
||||
"watch-messages": "paraglide-js compile --watch --project ./project.inlang --outdir ./src/paraglide"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/mdx": "^4.0.8",
|
||||
"@astrojs/node": "9.1.1",
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"@astrojs/svelte": "^7.0.4",
|
||||
"@iconify-json/pajamas": "^1.2.5",
|
||||
"@inlang/paraglide-astro": "^0.3.5",
|
||||
"@inlang/paraglide-js": "1.11.8",
|
||||
"@astrojs/mdx": "^4.3.0",
|
||||
"@astrojs/node": "9.2.2",
|
||||
"@astrojs/sitemap": "^3.4.1",
|
||||
"@astrojs/svelte": "^7.1.0",
|
||||
"@iconify-json/pajamas": "^1.2.11",
|
||||
"@inlang/paraglide-astro": "^0.4.1",
|
||||
"@inlang/paraglide-js": "2.1.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.0.9",
|
||||
"astro": "^5.4.1 ",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"astro": "^5.10.2",
|
||||
"astro-icon": "^1.1.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"sharp": "^0.33.5",
|
||||
"svelte": "^5.20.4",
|
||||
"tailwindcss": "^4.0.9",
|
||||
"typescript": "^5.7.3"
|
||||
"sharp": "^0.34.2",
|
||||
"svelte": "^5.34.9",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"daisyui": "^5.0.0",
|
||||
"prettier": "^3.5.2",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"vite": "^6.2.0"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"plugins": [
|
||||
"prettier-plugin-astro",
|
||||
"prettier-plugin-svelte"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "**/*.astro",
|
||||
"options": {
|
||||
"parser": "astro"
|
||||
}
|
||||
}
|
||||
]
|
||||
"daisyui": "^5.0.43",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
}
|
||||
|
2127
pnpm-lock.yaml
generated
2127
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@ let paths: string[]
|
||||
if (pathname === "/") {
|
||||
paths = ["~"]
|
||||
} else {
|
||||
paths = ["~", ...pathname.split("/").slice(1)]
|
||||
paths = ["~", ...pathname.split("/").filter(x => x)]
|
||||
}
|
||||
|
||||
function getLink(path: string): NavLink {
|
||||
|
@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
// TODO move to types?
|
||||
interface Option<Key> {
|
||||
key: Key
|
||||
value: string
|
||||
}
|
||||
// TODO move to types?
|
||||
interface Option<Key> {
|
||||
key: Key
|
||||
value: string
|
||||
}
|
||||
|
||||
interface Props<Key = string> {
|
||||
selected: Key
|
||||
options?: Option<Key>[]
|
||||
class?: string
|
||||
}
|
||||
interface Props<Key = string> {
|
||||
selected: Key
|
||||
options?: Option<Key>[]
|
||||
class?: string
|
||||
}
|
||||
|
||||
let { selected = $bindable(), options = [], class: clazz }: Props = $props()
|
||||
let { selected = $bindable(), options = [], class: clazz }: Props = $props()
|
||||
</script>
|
||||
|
||||
<select
|
||||
|
@ -1,80 +1,76 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let history: string[] = []
|
||||
let currentDir = "~"
|
||||
let history: string[] = []
|
||||
let currentDir = "~"
|
||||
|
||||
type Command = "help" | "about" | "skills" | "projects" | "contact" | "clear"
|
||||
type Command = "help" | "about" | "skills" | "projects" | "contact" | "clear"
|
||||
|
||||
const commands: Record<Command, () => string> = {
|
||||
help: () => `Available commands:
|
||||
const commands: Record<Command, () => string> = {
|
||||
help: () => `Available commands:
|
||||
about - Display information about me
|
||||
skills - List my technical skills
|
||||
projects - Show my notable projects
|
||||
contact - Display my contact information
|
||||
clear - Clear the terminal screen`,
|
||||
about: () => `Hi, I'm John Doe!
|
||||
about: () => `Hi, I'm John Doe!
|
||||
I'm a passionate software developer with 5 years of experience.
|
||||
I love creating elegant solutions to complex problems.`,
|
||||
skills: () => `My technical skills include:
|
||||
skills: () => `My technical skills include:
|
||||
- JavaScript/TypeScript
|
||||
- React & Next.js
|
||||
- Node.js
|
||||
- Python
|
||||
- SQL & NoSQL databases`,
|
||||
projects: () => `Some of my notable projects:
|
||||
projects: () => `Some of my notable projects:
|
||||
1. E-commerce Platform (React, Node.js, MongoDB)
|
||||
2. Weather App (React Native, OpenWeatherMap API)
|
||||
3. Task Management System (Python, Django, PostgreSQL)`,
|
||||
contact: () => `You can reach me at:
|
||||
contact: () => `You can reach me at:
|
||||
Email: john.doe@example.com
|
||||
GitHub: github.com/johndoe
|
||||
LinkedIn: linkedin.com/in/johndoe`,
|
||||
clear: () => {
|
||||
clear: () => {
|
||||
history = []
|
||||
return ""
|
||||
},
|
||||
}
|
||||
|
||||
const executeCommand = (input: string) => {
|
||||
const [command, ...args] = input.trim().split(" ")
|
||||
if (command in commands) {
|
||||
return commands[command as Command]()
|
||||
}
|
||||
return `Command not found: ${command}. Type 'help' for available commands.`
|
||||
}
|
||||
|
||||
let input = ""
|
||||
let inputRef: HTMLInputElement | null = null
|
||||
|
||||
const handleSubmit = (e: Event) => {
|
||||
e.preventDefault()
|
||||
if (input.trim()) {
|
||||
if (input === "clear") {
|
||||
history = []
|
||||
return ""
|
||||
},
|
||||
}
|
||||
|
||||
const executeCommand = (input: string) => {
|
||||
const [command, ...args] = input.trim().split(" ")
|
||||
if (command in commands) {
|
||||
return commands[command as Command]()
|
||||
} else {
|
||||
history = [...history, `${currentDir} $ ${input}`, executeCommand(input)]
|
||||
}
|
||||
return `Command not found: ${command}. Type 'help' for available commands.`
|
||||
input = ""
|
||||
}
|
||||
}
|
||||
|
||||
let input = ""
|
||||
let inputRef: HTMLInputElement | null = null
|
||||
onMount(() => {
|
||||
history = [
|
||||
"Welcome to John Doe's Terminal Portfolio!",
|
||||
"Type 'help' to see available commands.",
|
||||
]
|
||||
})
|
||||
|
||||
const handleSubmit = (e: Event) => {
|
||||
e.preventDefault()
|
||||
if (input.trim()) {
|
||||
if (input === "clear") {
|
||||
history = []
|
||||
} else {
|
||||
history = [
|
||||
...history,
|
||||
`${currentDir} $ ${input}`,
|
||||
executeCommand(input),
|
||||
]
|
||||
}
|
||||
input = ""
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
history = [
|
||||
"Welcome to John Doe's Terminal Portfolio!",
|
||||
"Type 'help' to see available commands.",
|
||||
]
|
||||
})
|
||||
|
||||
$: {
|
||||
if (inputRef) {
|
||||
inputRef.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
$: {
|
||||
if (inputRef) {
|
||||
inputRef.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-black text-green-500 p-4 font-mono">
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte"
|
||||
import type { Snippet } from "svelte"
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
children: Snippet
|
||||
}
|
||||
interface Props {
|
||||
title?: string
|
||||
children: Snippet
|
||||
}
|
||||
|
||||
const { title = "", children }: Props = $props()
|
||||
const { title = "", children }: Props = $props()
|
||||
</script>
|
||||
|
||||
<details class="collapse collapse-arrow bg-base-200">
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import Collapse from "@/components/collapse/Collapse.svelte"
|
||||
import Collapse from "@/components/collapse/Collapse.svelte"
|
||||
|
||||
interface Props {
|
||||
items?: string[]
|
||||
title?: string
|
||||
}
|
||||
interface Props {
|
||||
items?: string[]
|
||||
title?: string
|
||||
}
|
||||
|
||||
const { items = [], title = "" }: Props = $props()
|
||||
const { items = [], title = "" }: Props = $props()
|
||||
</script>
|
||||
|
||||
<Collapse {title}>
|
||||
|
@ -16,7 +16,10 @@ interface Props {
|
||||
const { project } = Astro.props
|
||||
|
||||
const entry = await getEntry("projects", project)
|
||||
const { Content } = await render(entry!)
|
||||
if (!entry) {
|
||||
throw new Error("Project not found")
|
||||
}
|
||||
const { Content } = await render(entry)
|
||||
const {
|
||||
lang,
|
||||
title,
|
||||
@ -28,7 +31,7 @@ const {
|
||||
source,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
} = entry!.data
|
||||
} = entry.data
|
||||
|
||||
function localeDateString(isoString: string): string {
|
||||
let template = "DD-MM-YYYY"
|
||||
|
@ -2,7 +2,7 @@ import { defineCollection, z } from "astro:content"
|
||||
import { glob } from "astro/loaders"
|
||||
|
||||
const projectCollection = defineCollection({
|
||||
loader: glob({ pattern: "**\/*.mdx", base: "./src/content/projects" }),
|
||||
loader: glob({ pattern: "**/*.mdx", base: "./src/content/projects" }),
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
lang: z.union([z.literal("en"), z.literal("nb")]),
|
||||
@ -19,7 +19,7 @@ const projectCollection = defineCollection({
|
||||
})
|
||||
|
||||
const usesCollection = defineCollection({
|
||||
loader: glob({ pattern: "**\/*.yaml", base: "./src/content/uses" }),
|
||||
loader: glob({ pattern: "**/*.yaml", base: "./src/content/uses" }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
accessories: z.optional(z.array(z.string())),
|
||||
|
@ -4,50 +4,50 @@
|
||||
@plugin "daisyui";
|
||||
|
||||
@theme {
|
||||
--color-cat-rosewater: #f5e0dc;
|
||||
--color-cat-flamingo: #f2cdcd;
|
||||
--color-cat-pink: #f5c2e7;
|
||||
--color-cat-mauve: #cba6f7;
|
||||
--color-cat-red: #f38ba8;
|
||||
--color-cat-maroon: #eba0ac;
|
||||
--color-cat-peach: #fab387;
|
||||
--color-cat-yellow: #f9e2af;
|
||||
--color-cat-green: #a6e3a1;
|
||||
--color-cat-teal: #94e2d5;
|
||||
--color-cat-sky: #89dceb;
|
||||
--color-cat-sapphire: #74c7ec;
|
||||
--color-cat-blue: #89b4fa;
|
||||
--color-cat-lavender: #b4befe;
|
||||
--color-cat-text: #cdd6f4;
|
||||
--color-cat-surface0: #313244;
|
||||
--color-cat-base: #1e1e2e;
|
||||
--color-cat-mantle: #181825;
|
||||
--color-cat-rosewater: #f5e0dc;
|
||||
--color-cat-flamingo: #f2cdcd;
|
||||
--color-cat-pink: #f5c2e7;
|
||||
--color-cat-mauve: #cba6f7;
|
||||
--color-cat-red: #f38ba8;
|
||||
--color-cat-maroon: #eba0ac;
|
||||
--color-cat-peach: #fab387;
|
||||
--color-cat-yellow: #f9e2af;
|
||||
--color-cat-green: #a6e3a1;
|
||||
--color-cat-teal: #94e2d5;
|
||||
--color-cat-sky: #89dceb;
|
||||
--color-cat-sapphire: #74c7ec;
|
||||
--color-cat-blue: #89b4fa;
|
||||
--color-cat-lavender: #b4befe;
|
||||
--color-cat-text: #cdd6f4;
|
||||
--color-cat-surface0: #313244;
|
||||
--color-cat-base: #1e1e2e;
|
||||
--color-cat-mantle: #181825;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.debug {
|
||||
@apply border border-red-500;
|
||||
}
|
||||
.debug {
|
||||
@apply border border-red-500;
|
||||
}
|
||||
}
|
||||
|
||||
br {
|
||||
@apply my-0.5;
|
||||
@apply my-0.5;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl font-bold mb-2;
|
||||
@apply text-4xl font-bold mb-2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-3xl font-bold mb-2;
|
||||
@apply text-3xl font-bold mb-2;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-2xl font-bold mb-2;
|
||||
@apply text-2xl font-bold mb-2;
|
||||
}
|
||||
|
||||
/* TODO change default style*/
|
||||
a {
|
||||
@apply link text-cat-mauve;
|
||||
text-decoration-line: none;
|
||||
@apply link text-cat-mauve;
|
||||
text-decoration-line: none;
|
||||
}
|
||||
|
@ -52,9 +52,8 @@ export function localizePathname(
|
||||
const rest = pathnameParts.slice(2)
|
||||
if (rest.length > 0) {
|
||||
return `${localizedPathname}/${rest.join("/")}`
|
||||
} else {
|
||||
return localizedPathname
|
||||
}
|
||||
return localizedPathname
|
||||
}
|
||||
return pathname
|
||||
}
|
||||
@ -71,13 +70,14 @@ export function isAbsolutePathname(path: string): path is AbsolutePathname {
|
||||
}
|
||||
|
||||
export function isNavLink(path: string): path is NavLink {
|
||||
let basePath = path
|
||||
if (path.startsWith("/en")) {
|
||||
path = path.slice(2)
|
||||
basePath = path.slice(2)
|
||||
}
|
||||
if (paths.has(path as NavLink)) {
|
||||
if (paths.has(basePath as NavLink)) {
|
||||
return true
|
||||
}
|
||||
const pathSplit = path.split("/").slice(1)
|
||||
const pathSplit = basePath.split("/").slice(1)
|
||||
return (
|
||||
pathSplit.length === 2 &&
|
||||
pathSplit[0] === "projects" &&
|
||||
|
Reference in New Issue
Block a user