This commit is contained in:
Morten Olsen
2024-04-19 20:51:52 +02:00
committed by Morten Olsen
parent 98e39a54cc
commit ee37ac9d90
51 changed files with 604 additions and 798 deletions

View File

@@ -1,2 +1,3 @@
/node_modules/
/.astro/
/.vscode/

24
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,24 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
extends: ['plugin:astro/recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
sourceType: 'module',
ecmaVersion: 'latest'
},
overrides: [
{
files: ['*.astro'],
parser: 'astro-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
extraFileExtensions: ['.astro']
},
rules: {
// override/add rules settings here, such as:
// "astro/no-set-html-directive": "error"
}
}
]
}

View File

@@ -1,7 +1,8 @@
name: Deploy Astro site to Pages
on:
push:
branches: [$default-branch]
branches:
- main
workflow_dispatch:
permissions:

13
.prettierrc.cjs Normal file
View File

@@ -0,0 +1,13 @@
/** @type {import("prettier").Config} */
module.exports = {
...require('prettier-config-standard'),
plugins: [require.resolve('prettier-plugin-astro')],
overrides: [
{
files: '*.astro',
options: {
parser: 'astro'
}
}
]
}

View File

@@ -1,14 +0,0 @@
/** @type {import("prettier").Config} */
export default {
plugins: ['prettier-plugin-astro'],
overrides: [
{
files: '*.astro',
options: {
parser: 'astro',
},
},
],
printWidth: 100,
singleQuote: true,
};

View File

@@ -1,4 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"recommendations": ["astro-build.astro-vscode", "esbenp.prettier-vscode"],
"unwantedRecommendations": []
}

13
.vscode/settings.json vendored
View File

@@ -1,3 +1,14 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.validate": [
"javascript",
"javascriptreact",
"astro",
"typescript",
"typescriptreact"
],
"prettier.documentSelectors": ["**/*.astro"],
"[astro]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View File

@@ -1,20 +1,20 @@
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import icon from "astro-icon";
import compress from "astro-compress";
import robotsTxt from 'astro-robots-txt';
import { defineConfig } from 'astro/config'
import mdx from '@astrojs/mdx'
import sitemap from '@astrojs/sitemap'
import icon from 'astro-icon'
import compress from 'astro-compress'
import robotsTxt from 'astro-robots-txt'
const getSiteInfo = () => {
const siteUrl = process.env.SITE_URL;
const siteUrl = process.env.SITE_URL
if (!siteUrl) {
return {};
return {}
}
const url = new URL(siteUrl);
const url = new URL(siteUrl)
return {
site: `${url.protocol}//${url.host}`,
base: url.pathname
};
};
}
}
// https://astro.build/config
export default defineConfig({
@@ -26,4 +26,4 @@ export default defineConfig({
assetsInlineLimit: 1024 * 10
}
}
});
})

View File

@@ -1,39 +0,0 @@
import eslint from '@eslint/js';
import eslintPluginAstro from 'eslint-plugin-astro';
import eslintPluginImport from 'eslint-plugin-import';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
/** @type {import('eslint').ESLint.ConfigData}*/
export default [
eslint.configs.recommended,
{
plugins: {
'simple-import-sort': simpleImportSort,
},
rules: {
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
},
},
{
plugins: {
import: eslintPluginImport,
},
rules: {
// "import/no-unresolved": "off",
'import/first': 'error',
'import/newline-after-import': 'error',
'import/no-duplicates': 'error',
},
},
eslintPluginPrettierRecommended,
...eslintPluginAstro.configs['flat/all'],
{
rules: {
'astro/no-set-html-directive': 'off',
'astro/no-unused-css-selector': 'off',
'one-var': ['error', 'never'],
},
},
];

View File

@@ -5,7 +5,8 @@
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"lint:apply": "eslint ./src --fix && prettier --write ./src",
"lint": "prettier \"**/*.{js,jsx,ts,tsx,md,mdx,astro}\" && eslint \"src/**/*.{js,ts,jsx,tsx,astro}\"",
"lint:apply": "prettier --write \"**/*.{js,jsx,ts,tsx,md,mdx,astro}\" && eslint --fix \"src/**/*.{js,ts,jsx,tsx,astro}\"",
"build": "astro check && astro build",
"preview": "astro preview",
"serve": "SITE_URL=http://localhost:3000 pnpm build && serve dist",
@@ -35,17 +36,14 @@
"astro-icon": "^1.1.0",
"astro-robots-txt": "^1.0.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-astro": "^0.33.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-simple-import-sort": "^12.0.0",
"husky": "^9.0.11",
"json-schema-to-typescript": "^13.1.2",
"less": "^4.2.0",
"lint-staged": "^15.2.2",
"prettier": "3.2.5",
"prettier": "^3.2.5",
"prettier-config-standard": "^7.0.0",
"prettier-plugin-astro": "^0.13.0",
"sass": "^1.72.0",
"serve": "^14.2.1",

209
pnpm-lock.yaml generated
View File

@@ -58,24 +58,12 @@ devDependencies:
eslint:
specifier: ^8.57.0
version: 8.57.0
eslint-config-prettier:
specifier: ^9.1.0
version: 9.1.0(eslint@8.57.0)
eslint-plugin-astro:
specifier: ^0.33.1
version: 0.33.1(eslint@8.57.0)
eslint-plugin-import:
specifier: ^2.29.1
version: 2.29.1(@typescript-eslint/parser@7.5.0)(eslint@8.57.0)
eslint-plugin-jsx-a11y:
specifier: ^6.8.0
version: 6.8.0(eslint@8.57.0)
eslint-plugin-prettier:
specifier: ^5.1.3
version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5)
eslint-plugin-simple-import-sort:
specifier: ^12.0.0
version: 12.0.0(eslint@8.57.0)
husky:
specifier: ^9.0.11
version: 9.0.11
@@ -89,8 +77,11 @@ devDependencies:
specifier: ^15.2.2
version: 15.2.2
prettier:
specifier: 3.2.5
specifier: ^3.2.5
version: 3.2.5
prettier-config-standard:
specifier: ^7.0.0
version: 7.0.0(prettier@3.2.5)
prettier-plugin-astro:
specifier: ^0.13.0
version: 0.13.0
@@ -2328,10 +2319,6 @@ packages:
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
dev: true
/@types/json5@0.0.29:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true
/@types/jsonld@1.5.13:
resolution: {integrity: sha512-n7fUU6W4kSYK8VQlf/LsE9kddBHPKhODoVOjsZswmve+2qLwBy6naWxs/EiuSZN9NU0N06Ra01FR+j87C62T0A==}
dev: true
@@ -2725,18 +2712,6 @@ packages:
engines: {node: '>=8'}
dev: true
/array.prototype.findlastindex@1.2.5:
resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.23.2
es-errors: 1.3.0
es-object-atoms: 1.0.0
es-shim-unscopables: 1.0.2
dev: true
/array.prototype.flat@1.3.2:
resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==}
engines: {node: '>= 0.4'}
@@ -3583,17 +3558,6 @@ packages:
ms: 2.0.0
dev: true
/debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.2
dev: true
/debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
@@ -3715,13 +3679,6 @@ packages:
/dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
/doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
dependencies:
esutils: 2.0.3
dev: true
/doctrine@3.0.0:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'}
@@ -4026,54 +3983,6 @@ packages:
semver: 7.6.0
dev: true
/eslint-config-prettier@9.1.0(eslint@8.57.0):
resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
hasBin: true
peerDependencies:
eslint: '>=7.0.0'
dependencies:
eslint: 8.57.0
dev: true
/eslint-import-resolver-node@0.3.9:
resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
dependencies:
debug: 3.2.7
is-core-module: 2.13.1
resolve: 1.22.8
transitivePeerDependencies:
- supports-color
dev: true
/eslint-module-utils@2.8.1(@typescript-eslint/parser@7.5.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0):
resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==}
engines: {node: '>=4'}
peerDependencies:
'@typescript-eslint/parser': '*'
eslint: '*'
eslint-import-resolver-node: '*'
eslint-import-resolver-typescript: '*'
eslint-import-resolver-webpack: '*'
peerDependenciesMeta:
'@typescript-eslint/parser':
optional: true
eslint:
optional: true
eslint-import-resolver-node:
optional: true
eslint-import-resolver-typescript:
optional: true
eslint-import-resolver-webpack:
optional: true
dependencies:
'@typescript-eslint/parser': 7.5.0(eslint@8.57.0)(typescript@5.4.2)
debug: 3.2.7
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
transitivePeerDependencies:
- supports-color
dev: true
/eslint-plugin-astro@0.33.1(eslint@8.57.0):
resolution: {integrity: sha512-wVyxAf8Ulmljv5qJQLgspWe17LR4hLXcksIENtUlEC3W7rleBVEKXS+hIqzBfCbpkBLZpl1tsYes1AGpYHd13w==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -4093,41 +4002,6 @@ packages:
- supports-color
dev: true
/eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.5.0)(eslint@8.57.0):
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
engines: {node: '>=4'}
peerDependencies:
'@typescript-eslint/parser': '*'
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
peerDependenciesMeta:
'@typescript-eslint/parser':
optional: true
dependencies:
'@typescript-eslint/parser': 7.5.0(eslint@8.57.0)(typescript@5.4.2)
array-includes: 3.1.8
array.prototype.findlastindex: 1.2.5
array.prototype.flat: 1.3.2
array.prototype.flatmap: 1.3.2
debug: 3.2.7
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.5.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3
minimatch: 3.1.2
object.fromentries: 2.0.8
object.groupby: 1.0.3
object.values: 1.2.0
semver: 6.3.1
tsconfig-paths: 3.15.0
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
dev: true
/eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0):
resolution: {integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==}
engines: {node: '>=4.0'}
@@ -4153,35 +4027,6 @@ packages:
object.fromentries: 2.0.8
dev: true
/eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5):
resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
'@types/eslint': '>=8.0.0'
eslint: '>=8.0.0'
eslint-config-prettier: '*'
prettier: '>=3.0.0'
peerDependenciesMeta:
'@types/eslint':
optional: true
eslint-config-prettier:
optional: true
dependencies:
eslint: 8.57.0
eslint-config-prettier: 9.1.0(eslint@8.57.0)
prettier: 3.2.5
prettier-linter-helpers: 1.0.0
synckit: 0.8.8
dev: true
/eslint-plugin-simple-import-sort@12.0.0(eslint@8.57.0):
resolution: {integrity: sha512-8o0dVEdAkYap0Cn5kNeklaKcT1nUsa3LITWEuFk3nJifOoD+5JQGoyDUW2W/iPWwBsNBJpyJS9y4je/BgxLcyQ==}
peerDependencies:
eslint: '>=5.0.0'
dependencies:
eslint: 8.57.0
dev: true
/eslint-scope@7.2.2:
resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -4411,10 +4256,6 @@ packages:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
/fast-diff@1.3.0:
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
dev: true
/fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
requiresBuild: true
@@ -5461,13 +5302,6 @@ packages:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
dev: true
/json5@1.0.2:
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
hasBin: true
dependencies:
minimist: 1.2.8
dev: true
/json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
@@ -6620,15 +6454,6 @@ packages:
es-object-atoms: 1.0.0
dev: true
/object.groupby@1.0.3:
resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.23.2
dev: true
/object.values@1.2.0:
resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==}
engines: {node: '>= 0.4'}
@@ -6931,11 +6756,12 @@ packages:
engines: {node: '>= 0.8.0'}
dev: true
/prettier-linter-helpers@1.0.0:
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
engines: {node: '>=6.0.0'}
/prettier-config-standard@7.0.0(prettier@3.2.5):
resolution: {integrity: sha512-NgZy4TYupJR6aMMuV/Aqs0ONnVhlFT8PXVkYRskxREq8EUhJHOddVfBxPV6fWpgcASpJSgvvhVLk0CBO5M3Hvw==}
peerDependencies:
prettier: ^2.6.0 || ^3.0.0
dependencies:
fast-diff: 1.3.0
prettier: 3.2.5
dev: true
/prettier-plugin-astro@0.13.0:
@@ -7890,14 +7716,6 @@ packages:
picocolors: 1.0.0
dev: true
/synckit@0.8.8:
resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==}
engines: {node: ^14.18.0 || >=16.0.0}
dependencies:
'@pkgr/core': 0.1.1
tslib: 2.6.2
dev: true
/synckit@0.9.0:
resolution: {integrity: sha512-7RnqIMq572L8PeEzKeBINYEJDDxpcH8JEgLwUqBd3TkofhFRbkq4QLR0u+36avGAhCRbk2nnmjcW9SE531hPDg==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -8057,15 +7875,6 @@ packages:
dependencies:
typescript: 5.4.2
/tsconfig-paths@3.15.0:
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
dependencies:
'@types/json5': 0.0.29
json5: 1.0.2
minimist: 1.2.8
strip-bom: 3.0.0
dev: true
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
requiresBuild: true

View File

@@ -1,16 +1,18 @@
import { fileURLToPath} from 'url';
import { resolve, dirname } from 'path';
import { writeFile, mkdir } from 'fs/promises';
import { compile } from 'json-schema-to-typescript';
import { fileURLToPath } from 'url'
import { resolve, dirname } from 'path'
import { writeFile, mkdir } from 'fs/promises'
import { compile } from 'json-schema-to-typescript'
const root = fileURLToPath(new URL('..', import.meta.url));
const root = fileURLToPath(new URL('..', import.meta.url))
const response = await fetch('https://raw.githubusercontent.com/jsonresume/resume-schema/master/schema.json');
const schema = await response.json();
const response = await fetch(
'https://raw.githubusercontent.com/jsonresume/resume-schema/master/schema.json'
)
const schema = await response.json()
const types = await compile(schema, 'ResumeSchema', { bannerComment: '' });
const types = await compile(schema, 'ResumeSchema', { bannerComment: '' })
const location = resolve(root, 'src/types/resume-schema.ts');
console.log(`Writing to ${location}`);
await mkdir(dirname(location), { recursive: true });
await writeFile(location, types);
const location = resolve(root, 'src/types/resume-schema.ts')
console.log(`Writing to ${location}`)
await mkdir(dirname(location), { recursive: true })
await writeFile(location, types)

View File

@@ -1,7 +1,7 @@
import { getImage } from 'astro:assets';
import { data } from '@/data/data.js';
import { getImage } from 'astro:assets'
import { data } from '@/data/data.js'
const imageSizes = [16, 32, 48, 64, 96, 128, 256, 512];
const imageSizes = [16, 32, 48, 64, 96, 128, 256, 512]
const pngs = await Promise.all(
imageSizes.map(async (size) => {
@@ -10,14 +10,14 @@ const pngs = await Promise.all(
src: data.profile.image,
format: 'png',
width: size,
height: size,
height: size
})),
size: `${size}x${size}`,
};
}),
);
size: `${size}x${size}`
}
})
)
const icons = {
pngs,
};
export { icons };
pngs
}
export { icons }

View File

@@ -1,15 +1,15 @@
---
import { data } from '@/data/data';
import { data } from '@/data/data'
const { basics } = data.profile;
const { basics } = data.profile
---
<nav>
<a href="/">{basics.name}</a>
<a href='/'>{basics.name}</a>
<div>{basics.tagline}</div>
</nav>
<style>
<style lang='less'>
nav {
margin: 0 auto;
width: 100%;

View File

@@ -1,10 +1,10 @@
---
type Props = {
datetime: Date | undefined;
format?: Intl.DateTimeFormatOptions;
};
const { datetime, format, ...rest } = Astro.props;
const formatted = Intl.DateTimeFormat('en-US', format).format(datetime);
datetime: Date | undefined
format?: Intl.DateTimeFormatOptions
}
const { datetime, format, ...rest } = Astro.props
const formatted = Intl.DateTimeFormat('en-US', format).format(datetime)
---
{

View File

@@ -6,11 +6,11 @@ color: '#e7d9ac'
heroImage: ./assets/cover.png
---
import { Image } from 'astro:assets';
import TaskBounds from './assets/TaskBounds.png';
import Frame1 from './assets/Frame1.png';
import Graph1 from './assets/Graph1.png';
import Graph2 from './assets/Graph2.png';
import { Image } from 'astro:assets'
import TaskBounds from './assets/TaskBounds.png'
import Frame1 from './assets/Frame1.png'
import Graph1 from './assets/Graph1.png'
import Graph2 from './assets/Graph2.png'
Allow me to introduce Bob. Bob is an algorithm, and he has just accepted a role as my assistant.
@@ -26,7 +26,7 @@ Also, I wanted a planning algorithm that was not only for productivity. I did no
Bob is still pretty young and still learning new things, but he has gotten to the point where I believe he is good enough to start to use on a day to day basis.
<Image src={Frame1} alt="Frame1" />
<Image src={Frame1} alt='Frame1' />
How does Bob work? Bob gets a list of tasks, some from my calendar (both my work and my personal calendar), some from "routines" (which are daily tasks that I want to do most days, such as eating breakfast or picking up the kid), and some tasks come from "goals" which are a list of completable items. These tasks go into Bob, and he tries to create a plan for the next couple of days where I get everything done that I set out to do.
@@ -38,7 +38,7 @@ An "earliest start time" and a "latest start time". These define when the task c
- If the task is required.
- A priority
<Image src={TaskBounds} alt="Task bounds" />
<Image src={TaskBounds} alt='Task bounds' />
Bob uses a graph walk to create the optimal plan, where each node contains a few different things
@@ -53,11 +53,11 @@ Bob starts by figuring out which locations I can go to complete the remaining ta
He then gets a list of all the remaining tasks for the current node which can be completed at the current location, again figuring out when I would be done with the task, updating the list of impossible tasks and scoring the node.
If any node adds a required task to the impossible list, that node is considered dead, and Bob will not analyze it further.
<Image src={Graph1} alt="Graph1" />
<Image src={Graph1} alt='Graph1' />
Now we have a list of active leaves, and from that list, we find the node with the highest score and redo the process from above.
<Image src={Graph2} alt="Graph2" />
<Image src={Graph2} alt='Graph2' />
Bob has four different strategies for finding a plan.

View File

@@ -6,8 +6,8 @@ description: ''
heroImage: ./assets/cover.png
---
import graph from './assets/graph.png';
import { Image } from 'astro:assets';
import graph from './assets/graph.png'
import { Image } from 'astro:assets'
I have been playing around with smart homes for a long time; I have used most of the platforms out there, I have developed quite a few myself, and one thing I keep coming back to is Redux.
@@ -66,7 +66,7 @@ Now comes the part I have feared, where I need to draw a diagram.
...sorry
<Image src={graph} alt="graph" />
<Image src={graph} alt='graph' />
So this shows our final setup.

View File

@@ -1,4 +1,4 @@
import { defineCollection, z } from 'astro:content';
import { defineCollection, z } from 'astro:content'
const articles = defineCollection({
schema: ({ image }) =>
@@ -10,10 +10,10 @@ const articles = defineCollection({
updatedDate: z.coerce.date().optional(),
tags: z.array(z.string()).optional(),
heroImage: image().refine((img) => img.width >= 320, {
message: 'Cover image must be at least 1080 pixels wide!',
}),
}),
});
message: 'Cover image must be at least 1080 pixels wide!'
})
})
})
const work = defineCollection({
schema: ({ image }) =>
@@ -26,16 +26,16 @@ const work = defineCollection({
url: z.string().optional(),
logo: image()
.refine((img) => img.width >= 200, {
message: 'Logo must be at least 320 pixels wide!',
message: 'Logo must be at least 320 pixels wide!'
})
.optional(),
banner: image()
.refine((img) => img.height >= 50, {
message: 'Logo must be at least 320 pixels wide!',
message: 'Logo must be at least 320 pixels wide!'
})
.optional(),
}),
});
.optional()
})
})
const references = defineCollection({
schema: () =>
@@ -45,16 +45,16 @@ const references = defineCollection({
company: z.string(),
date: z.coerce.date(),
relation: z.string(),
profile: z.string(),
}),
});
profile: z.string()
})
})
const skills = defineCollection({
schema: () =>
z.object({
name: z.string(),
technologies: z.array(z.string()),
}),
});
technologies: z.array(z.string())
})
})
export const collections = { articles, work, references, skills };
export const collections = { articles, work, references, skills }

View File

@@ -1,6 +1,6 @@
import type { ResumeSchema } from '@/types/resume-schema.js';
import { Content } from './description.md';
import image from './profile.jpg';
import type { ResumeSchema } from '@/types/resume-schema.js'
import { Content } from './description.md'
import image from './profile.jpg'
const basics = {
name: 'Morten Olsen',
@@ -11,38 +11,38 @@ const basics = {
location: {
city: 'Copenhagen',
countryCode: 'DK',
region: 'Capital Region of Denmark',
region: 'Capital Region of Denmark'
},
profiles: [
{
network: 'GitHub',
icon: 'mdi:github',
username: 'morten-olsen',
url: 'https://github.com/morten-olsen',
url: 'https://github.com/morten-olsen'
},
{
network: 'LinkedIn',
icon: 'mdi:linkedin',
username: 'mortenolsendk',
url: 'https://www.linkedin.com/in/mortenolsendk',
},
url: 'https://www.linkedin.com/in/mortenolsendk'
}
],
languages: [
{
name: 'English',
fluency: 'Conversational',
fluency: 'Conversational'
},
{
name: 'Danish',
fluency: 'Native speaker',
},
],
} satisfies ResumeSchema['basics'];
fluency: 'Native speaker'
}
]
} satisfies ResumeSchema['basics']
const profile = {
basics,
image,
Content,
};
Content
}
export { profile };
export { profile }

View File

@@ -1,13 +1,13 @@
import { getCollection } from 'astro:content';
import { getCollection } from 'astro:content'
class Articles {
public find = () => getCollection('articles');
public find = () => getCollection('articles')
public get = async (slug: string) => {
const collection = await this.find();
return collection.find((entry) => entry.slug === slug);
};
const collection = await this.find()
return collection.find((entry) => entry.slug === slug)
}
}
type Article = Exclude<Awaited<ReturnType<Articles['get']>>, undefined>;
type Article = Exclude<Awaited<ReturnType<Articles['get']>>, undefined>
export { Articles, type Article };
export { Articles, type Article }

View File

@@ -1,13 +1,13 @@
import { getCollection } from 'astro:content';
import { getCollection } from 'astro:content'
class References {
public find = () => getCollection('references');
public find = () => getCollection('references')
public get = async (slug: string) => {
const collection = await this.find();
return collection.find((entry) => entry.slug === slug);
};
const collection = await this.find()
return collection.find((entry) => entry.slug === slug)
}
}
type Reference = Exclude<Awaited<ReturnType<References['get']>>, undefined>;
type Reference = Exclude<Awaited<ReturnType<References['get']>>, undefined>
export { References, type Reference };
export { References, type Reference }

View File

@@ -1,5 +1,5 @@
const site = {
theme: '#30E130',
};
theme: '#30E130'
}
export { site };
export { site }

View File

@@ -1,13 +1,13 @@
import { getCollection } from 'astro:content';
import { getCollection } from 'astro:content'
class Skills {
public find = () => getCollection('skills');
public find = () => getCollection('skills')
public get = async (slug: string) => {
const collection = await this.find();
return collection.find((entry) => entry.slug === slug);
};
const collection = await this.find()
return collection.find((entry) => entry.slug === slug)
}
}
type Skill = Exclude<Awaited<ReturnType<Skills['get']>>, undefined>;
type Skill = Exclude<Awaited<ReturnType<Skills['get']>>, undefined>
export { Skills, type Skill };
export { Skills, type Skill }

View File

@@ -1,25 +1,25 @@
import { profile } from '../content/profile/profile.js';
import { type Article, Articles } from './data.articles.js';
import { References } from './data.references.ts';
import { site } from './data.site.ts';
import { Skills } from './data.skills.ts';
import { getJsonLDResume, getJsonResume } from './data.utils.js';
import { Work, type WorkItem } from './data.work.js';
import { profile } from '../content/profile/profile.js'
import { type Article, Articles } from './data.articles.js'
import { References } from './data.references.ts'
import { site } from './data.site.ts'
import { Skills } from './data.skills.ts'
import { getJsonLDResume, getJsonResume } from './data.utils.js'
import { Work, type WorkItem } from './data.work.js'
class Data {
public articles = new Articles();
public work = new Work();
public references = new References();
public skills = new Skills();
public profile = profile;
public site = site;
public articles = new Articles()
public work = new Work()
public references = new References()
public skills = new Skills()
public profile = profile
public site = site
public getJsonResume = getJsonResume.bind(null, this);
public getJsonLDResume = getJsonLDResume.bind(null, this);
public getJsonResume = getJsonResume.bind(null, this)
public getJsonLDResume = getJsonLDResume.bind(null, this)
}
const data = new Data();
const data = new Data()
type Profile = typeof profile;
export type { Article, Profile, WorkItem };
export { data, Data };
type Profile = typeof profile
export type { Article, Profile, WorkItem }
export { data, Data }

View File

@@ -1,15 +1,15 @@
import type { ResumeSchema } from '@/types/resume-schema.js';
import type { ResumeSchema } from '@/types/resume-schema.js'
import type { Article, Data } from './data';
import type { Article, Data } from './data'
const getJsonResume = async (data: Data) => {
const profile = data.profile;
const profile = data.profile
const resume = {
basics: profile.basics,
} satisfies ResumeSchema;
basics: profile.basics
} satisfies ResumeSchema
return resume;
};
return resume
}
const getArticleJsonLD = async (data: Data, article: Article) => {
const jsonld = {
@@ -23,16 +23,16 @@ const getArticleJsonLD = async (data: Data, article: Article) => {
author: {
'@type': 'Person',
name: data.profile.basics.name,
url: data.profile.basics.url,
},
};
return jsonld;
};
url: data.profile.basics.url
}
}
return jsonld
}
const getJsonLDResume = async (data: Data) => {
const work = await data.work.find();
const currentWork = work.find((w) => !w.data.endDate);
const otherWork = work.filter((w) => w !== currentWork);
const work = await data.work.find()
const currentWork = work.find((w) => !w.data.endDate)
const otherWork = work.filter((w) => w !== currentWork)
const jsonld = {
'@context': 'https://schema.org',
@@ -47,24 +47,24 @@ const getJsonLDResume = async (data: Data) => {
'@type': 'ContactPoint',
contactType: profile.network.toLowerCase(),
identifier: profile.username,
url: profile.url,
url: profile.url
})),
address: {
'@type': 'PostalAddress',
addressLocality: data.profile.basics.location.city,
addressRegion: data.profile.basics.location.region,
addressCountry: data.profile.basics.location.countryCode,
addressCountry: data.profile.basics.location.countryCode
},
sameAs: data.profile.basics.profiles.map((profile) => profile.url),
hasOccupation: currentWork && {
'@type': 'EmployeeRole',
roleName: currentWork.data.position,
startDate: currentWork.data.startDate.toISOString(),
startDate: currentWork.data.startDate.toISOString()
},
worksFor: currentWork && {
'@type': 'Organization',
name: currentWork?.data.name,
sameAs: currentWork?.data.url,
sameAs: currentWork?.data.url
},
alumniOf: otherWork.map((w) => ({
'@type': 'Organization',
@@ -76,14 +76,14 @@ const getJsonLDResume = async (data: Data) => {
'@type': 'EmployeeRole',
roleName: w.data.position,
startDate: w.data.startDate.toISOString(),
endDate: w.data.endDate?.toISOString(),
endDate: w.data.endDate?.toISOString()
},
sameAs: '#me',
},
})),
};
sameAs: '#me'
}
}))
}
return jsonld;
};
return jsonld
}
export { getJsonResume, getJsonLDResume, getArticleJsonLD };
export { getJsonResume, getJsonLDResume, getArticleJsonLD }

View File

@@ -1,12 +1,12 @@
import { getCollection } from 'astro:content';
import { getCollection } from 'astro:content'
class Work {
public find = () => getCollection('work');
public find = () => getCollection('work')
public get = async (slug: string) => {
const collection = await this.find();
return collection.find((entry) => entry.slug === slug);
};
const collection = await this.find()
return collection.find((entry) => entry.slug === slug)
}
}
type WorkItem = Exclude<Awaited<ReturnType<Work['get']>>, undefined>;
export { Work, type WorkItem };
type WorkItem = Exclude<Awaited<ReturnType<Work['get']>>, undefined>
export { Work, type WorkItem }

View File

@@ -1,17 +1,17 @@
---
import { Picture } from 'astro:assets';
import { Picture } from 'astro:assets'
import { type Article, data } from '@/data/data.js';
import { getArticleJsonLD } from '@/data/data.utils';
import { type Article, data } from '@/data/data.js'
import { getArticleJsonLD } from '@/data/data.utils'
import Html from '../html/html.astro';
import Html from '../html/html.astro'
type Props = {
article: Article;
};
const { props } = Astro;
const { article } = props;
const { Content } = await article.render();
article: Article
}
const { props } = Astro
const { article } = props
const { Content } = await article.render()
---
<Html
@@ -24,22 +24,22 @@ const { Content } = await article.render();
<h1>
{article.data.title.split(' ').map((word) => <span>{word}</span>)}
</h1>
<a href="/"><h2>By {data.profile.basics.name}</h2></a>
<a href='/'><h2>By {data.profile.basics.name}</h2></a>
</header>
<Picture
loading="eager"
class="img"
loading='eager'
class='img'
src={article.data.heroImage}
widths={[320, 640, 1024, 1400]}
formats={['avif', 'webp', 'png']}
alt="Cover image"
alt='Cover image'
/>
<div class="content">
<div class='content'>
<Content />
</div>
</article>
</Html>
<style lang="less">
<style lang='less'>
article {
--left-padding: 100px;
display: grid;

View File

@@ -1,20 +1,20 @@
---
import type { Article } from '@/data/data.js';
import { range } from '@/utils/data';
import type { Article } from '@/data/data.js'
import { range } from '@/utils/data'
import Html from '../html/html.astro';
import Html from '../html/html.astro'
type Props = {
pageNumber: number;
pageCount: number;
articles: Article[];
};
const { articles, pageNumber, pageCount } = Astro.props;
const hasPrev = pageNumber > 1;
const hasNext = pageNumber < pageCount;
pageNumber: number
pageCount: number
articles: Article[]
}
const { articles, pageNumber, pageCount } = Astro.props
const hasPrev = pageNumber > 1
const hasNext = pageNumber < pageCount
---
<Html title="Articles" description="A list of articles">
<Html title='Articles' description='A list of articles'>
<h1>Articles</h1>
{
articles.map((article) => (
@@ -26,7 +26,9 @@ const hasNext = pageNumber < pageCount;
}
<nav>
<a aria-disabled={!hasPrev} href={`/articles/pages/${pageNumber - 1}`}>Previous</a>
<a aria-disabled={!hasPrev} href={`/articles/pages/${pageNumber - 1}`}
>Previous</a
>
{
range(1, pageCount).map((page) => (
<a
@@ -37,11 +39,13 @@ const hasNext = pageNumber < pageCount;
</a>
))
}
<a aria-disabled={!hasNext} href={`/articles/pages/${pageNumber + 1}`}> Next </a>
<a aria-disabled={!hasNext} href={`/articles/pages/${pageNumber + 1}`}>
Next
</a>
</nav>
</Html>
<style>
<style lang='less'>
nav {
display: flex;
justify-content: center;

View File

@@ -1,31 +1,32 @@
---
import { data } from '@/data/data.js';
import { data } from '@/data/data.js'
import Article from './articles.item.astro';
import Article from './articles.item.astro'
type Props = {
class?: string;
};
class?: string
}
const { class: className, ...rest } = Astro.props;
const articleCount = 6;
const allArticles = await data.articles.find();
const { class: className, ...rest } = Astro.props
const articleCount = 6
const allArticles = await data.articles.find()
const sortedArticles = allArticles.sort(
(a, b) => new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime(),
);
const hasMore = sortedArticles.length > articleCount;
const articles = sortedArticles.slice(0, articleCount);
(a, b) =>
new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime()
)
const hasMore = sortedArticles.length > articleCount
const articles = sortedArticles.slice(0, articleCount)
---
<div class:list={['articles', className]} {...rest}>
<h2>Articles</h2>
<div class="items">
<div class='items'>
{articles.map((article) => <Article article={article} />)}
</div>
{hasMore && <a href="/articles/pages/1">View all articles</a>}
{hasMore && <a href='/articles/pages/1'>View all articles</a>}
</div>
<style>
<style lang='less'>
.articles {
display: grid;
gap: var(--space-lg);

View File

@@ -1,27 +1,27 @@
---
import { Picture } from 'astro:assets';
import { Picture } from 'astro:assets'
import Time from '@/components/time/absolute.astro';
import type { Article } from '@/data/data.js';
import { formatDate } from '@/utils/time.js';
import Time from '@/components/time/absolute.astro'
import type { Article } from '@/data/data.js'
import { formatDate } from '@/utils/time.js'
type Props = {
article: Article;
};
article: Article
}
const { article: item } = Astro.props;
const { article: item } = Astro.props
---
<a href={`/articles/${item.slug}`}>
<article>
<Picture
class="thumb"
alt="thumbnail image"
class='thumb'
alt='thumbnail image'
src={item.data.heroImage}
formats={['avif', 'webp', 'jpeg']}
width={100}
/>
<div class="content">
<div class='content'>
<small>
<Time format={formatDate} datetime={item.data.pubDate} />
</small>
@@ -30,7 +30,7 @@ const { article: item } = Astro.props;
</article>
</a>
<style>
<style lang='less'>
a {
width: 45%;
}

View File

@@ -1,37 +1,37 @@
---
import { Picture } from 'astro:assets';
import { Picture } from 'astro:assets'
import { data } from '@/data/data.js';
import { data } from '@/data/data.js'
import Profile from './description.profile.astro';
import Profile from './description.profile.astro'
type Props = {
class?: string;
};
class?: string
}
const { class: className, ...rest } = Astro.props;
const { Content, basics, image } = data.profile;
const { class: className, ...rest } = Astro.props
const { Content, basics, image } = data.profile
---
<div class:list={['main', className]} {...rest}>
<Picture
class="picture"
alt="Profile Picture"
class='picture'
alt='Profile Picture'
src={image}
formats={['avif', 'webp', 'jpeg']}
width={230}
/>
<h1>{basics.name}</h1>
<h2>{basics.tagline}</h2>
<div class="description">
<div class='description'>
<Content />
</div>
<div class="profiles">
<div class='profiles'>
{basics.profiles.map((profile) => <Profile profile={profile} />)}
</div>
</div>
<style>
<style lang='less'>
@media screen and (max-width: 768px) {
.main {
display: flex;

View File

@@ -1,21 +1,21 @@
---
import { Icon } from 'astro-icon/components';
import { Icon } from 'astro-icon/components'
import type { Profile } from '@/data/data';
import type { Profile } from '@/data/data'
type Props = {
profile: Profile['basics']['profiles'][number];
};
const { profile } = Astro.props;
profile: Profile['basics']['profiles'][number]
}
const { profile } = Astro.props
---
<a href={profile.url} target="_blank">
<Icon class="icon" name={profile.icon} />
<div class="network">{profile.network}</div>
<div class="username">{profile.username}</div>
<a href={profile.url} target='_blank'>
<Icon class='icon' name={profile.icon} />
<div class='network'>{profile.network}</div>
<div class='username'>{profile.username}</div>
</a>
<style>
<style lang='less'>
a {
display: grid;
align-items: center;

View File

@@ -1,29 +1,33 @@
---
import { data } from '@/data/data';
import { data } from '@/data/data'
import Html from '../html/html.astro';
import Articles from './articles/articles.astro';
import Description from './description/description.astro';
import Info from './info/info.astro';
import Skills from './skills/skills.astro';
import Work from './work/work.astro';
import Html from '../html/html.astro'
import Articles from './articles/articles.astro'
import Description from './description/description.astro'
import Info from './info/info.astro'
import Skills from './skills/skills.astro'
import Work from './work/work.astro'
const jsonLd = await data.getJsonLDResume();
const jsonLd = await data.getJsonLDResume()
---
<Html title={data.profile.basics.name} description="Landing page" jsonLd={jsonLd}>
<div class="wrapper">
<div class="frontpage">
<Description class="description" />
<Info class="info" />
<Articles class="articles" />
<Skills class="skills" />
<Work class="work" />
<Html
title={data.profile.basics.name}
description='Landing page'
jsonLd={jsonLd}
>
<div class='wrapper'>
<div class='frontpage'>
<Description class='description' />
<Info class='info' />
<Articles class='articles' />
<Skills class='skills' />
<Work class='work' />
</div>
</div>
</Html>
<style>
<style lang='less'>
.wrapper {
--gap: var(--space-xxl);
margin: 0 auto;

View File

@@ -1,12 +1,12 @@
---
import { data } from '@/data/data.js';
import { data } from '@/data/data.js'
type Props = {
class?: string;
};
class?: string
}
const { class: className, ...rest } = Astro.props;
const { basics } = data.profile;
const { class: className, ...rest } = Astro.props
const { basics } = data.profile
---
<div class:list={['sidebar', className]} {...rest}>
@@ -28,7 +28,7 @@ const { basics } = data.profile;
</div>
</div>
<style>
<style lang='less'>
.sidebar {
display: flex;
flex-direction: column;

View File

@@ -1,20 +1,20 @@
---
import { data } from '@/data/data.js';
import { data } from '@/data/data.js'
type Props = {
class?: string;
};
class?: string
}
const { class: className, ...rest } = Astro.props;
const skills = await data.skills.find();
const { class: className, ...rest } = Astro.props
const skills = await data.skills.find()
---
<div class:list={['skills', className]} {...rest}>
<h2>Skills</h2>
<div class="skill">
<div class='skill'>
{
skills.map((item) => (
<div class="item">
<div class='item'>
<h3>{item.data.name}</h3>
<ul>
{item.data.technologies.map((tech) => (
@@ -27,7 +27,7 @@ const skills = await data.skills.find();
</div>
</div>
<style>
<style lang='less'>
h2 {
font-size: var(--font-xl);
margin-bottom: var(--space-lg);

View File

@@ -1,31 +1,33 @@
---
import Time from '@/components/time/absolute.astro';
import { data } from '@/data/data.js';
import { formatYearMonth } from '@/utils/time.js';
import Time from '@/components/time/absolute.astro'
import { data } from '@/data/data.js'
import { formatYearMonth } from '@/utils/time.js'
type Props = {
class?: string;
};
class?: string
}
const { class: className, ...rest } = Astro.props;
const allWork = await data.work.find();
const { class: className, ...rest } = Astro.props
const allWork = await data.work.find()
const work = allWork.sort((a, b) => {
return new Date(b.data.startDate).getTime() - new Date(a.data.startDate).getTime();
});
return (
new Date(b.data.startDate).getTime() - new Date(a.data.startDate).getTime()
)
})
---
<div class:list={[className]} {...rest}>
<h2>Work</h2>
<div class="list">
<div class='list'>
{
work.map((item) => (
<div class="item">
<div class="time">
<div class='item'>
<div class='time'>
<Time format={formatYearMonth} datetime={item.data.endDate} />
<br />
<Time format={formatYearMonth} datetime={item.data.startDate} />
</div>
<div class="content">
<div class='content'>
<h3>{item.data.position}</h3>
<h4>@ {item.data.name}</h4>
<p>{item.data.summary}</p>
@@ -34,10 +36,10 @@ const work = allWork.sort((a, b) => {
))
}
</div>
<a href="/work-history">See full work history</a>
<a href='/work-history'>See full work history</a>
</div>
<style>
<style lang='less'>
h2 {
font-size: var(--font-xl);
margin-bottom: var(--space-lg);

View File

@@ -1,40 +1,53 @@
---
import '@/style/theme.css';
import '@/style/theme.css'
import { Head } from 'astro-capo';
import { Head } from 'astro-capo'
import { icons } from '@/assets/images/icons.js';
import { data } from '@/data/data.js';
import { icons } from '@/assets/images/icons.js'
import { data } from '@/data/data.js'
type Props = {
title: string;
description: string;
image?: string;
jsonLd?: unknown;
};
const { props } = Astro;
const schema = JSON.stringify(props.jsonLd);
title: string
description: string
image?: string
jsonLd?: unknown
}
const { props } = Astro
const schema = JSON.stringify(props.jsonLd)
---
<!doctype html>
<html lang="en">
<html lang='en'>
<Head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="HandheldFriendly" content="True" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta charset='UTF-8' />
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<meta name='HandheldFriendly' content='True' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>{props.title}</title>
<link rel="sitemap" href="/sitemap-index.xml" />
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="generator" content={Astro.generator} />
<meta name="theme-color" content={data.site.theme} />
<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="/articles/rss.xml" />
{props.description && <meta name="description" content={props.description} />}
{props.image && <meta property="og:image" content={props.image} />}
{props.jsonLd && <script type="application/ld+json" is:inline set:html={schema} />}
<link rel='sitemap' href='/sitemap-index.xml' />
<link rel='manifest' href='/manifest.webmanifest' />
<meta name='generator' content={Astro.generator} />
<meta name='theme-color' content={data.site.theme} />
<link
rel='alternate'
type='application/rss+xml'
title='RSS Feed'
href='/articles/rss.xml'
/>
{
props.description && (
<meta name='description' content={props.description} />
)
}
{props.image && <meta property='og:image' content={props.image} />}
{
props.jsonLd && (
<script type='application/ld+json' is:inline set:html={schema} />
)
}
{
icons.pngs.map((icon) => (
<link rel="icon" href={icon.src} type="image/png" sizes={icon.size} />
<link rel='icon' href={icon.src} type='image/png' sizes={icon.size} />
))
}
</Head>

View File

@@ -1,25 +1,27 @@
---
import Header from '@/components/header/header.astro';
import { data } from '@/data/data.js';
import Header from '@/components/header/header.astro'
import { data } from '@/data/data.js'
import Html from '../html/html.astro';
import Item from './work-history.item.astro';
import Html from '../html/html.astro'
import Item from './work-history.item.astro'
const allWork = await data.work.find();
const work = allWork.sort((a, b) => b.data.startDate.getTime() - a.data.startDate.getTime());
const allWork = await data.work.find()
const work = allWork.sort(
(a, b) => b.data.startDate.getTime() - a.data.startDate.getTime()
)
---
<Html title="Work" description="A list of work experiences">
<Html title='Work' description='A list of work experiences'>
<Header />
<div class="wrapper">
<div class='wrapper'>
<h2>Work history</h2>
<div class="list">
<div class='list'>
{work.map((item) => <Item item={item} />)}
</div>
</div>
</Html>
<style>
<style lang='less'>
.wrapper {
margin: 0 auto;
max-width: var(--content-width);

View File

@@ -1,32 +1,32 @@
---
import Time from '@/components/time/absolute.astro';
import type { WorkItem } from '@/data/data.js';
import { formatYearMonth } from '@/utils/time.js';
import Time from '@/components/time/absolute.astro'
import type { WorkItem } from '@/data/data.js'
import { formatYearMonth } from '@/utils/time.js'
type Props = {
item: WorkItem;
};
item: WorkItem
}
const { item } = Astro.props;
const { Content } = await item.render();
const { item } = Astro.props
const { Content } = await item.render()
---
<div class="item">
<div class="time">
<div class='item'>
<div class='time'>
<Time format={formatYearMonth} datetime={item.data.endDate} />
-
<Time format={formatYearMonth} datetime={item.data.startDate} />
</div>
<div class="main">
<div class='main'>
<h3>{item.data.position}</h3>
<h4>{item.data.name}</h4>
<div class="content">
<div class='content'>
<Content />
</div>
</div>
</div>
<style>
<style lang='less'>
.item {
display: contents;
break-inside: avoid;

View File

@@ -1,22 +1,20 @@
---
import { type Article, data } from '@/data/data.js';
import ArticleView from '@/layouts/article/article.astro';
import { type Article, data } from '@/data/data.js'
import ArticleView from '@/layouts/article/article.astro'
type Props = {
article: Article;
};
article: Article
}
const getStaticPaths = async () => {
const articles = await data.articles.find();
export const getStaticPaths = async () => {
const articles = await data.articles.find()
return articles.map((article) => ({
params: { slug: article.slug },
props: { article },
}));
};
const { props } = Astro;
const { article } = props;
export { getStaticPaths };
props: { article }
}))
}
const { props } = Astro
const { article } = props
---
<ArticleView article={article} />

View File

@@ -1,24 +0,0 @@
import { type Article, data } from '@/data/data.ts';
import type { APIContext } from 'astro';
type Props = {
article: Article;
};
export async function GET(context: APIContext<Props>) {
const { props } = context;
const { article } = props;
return new Response(JSON.stringify(article), {
headers: {
'Content-Type': 'application/json',
},
});
}
export async function getStaticPaths() {
const articles = await data.articles.find();
return articles.map((article) => ({
params: { slug: article.slug },
props: { article },
}));
}

View File

@@ -1,36 +1,36 @@
---
import { type Article, data } from '@/data/data.js';
import Articles from '@/layouts/articles/articles.astro';
import { range } from '@/utils/data.js';
import { type Article, data } from '@/data/data.js'
import Articles from '@/layouts/articles/articles.astro'
import { range } from '@/utils/data.js'
type Props = {
articles: Article[];
pageNumber: number;
pageCount: number;
pageSize: number;
};
articles: Article[]
pageNumber: number
pageCount: number
pageSize: number
}
export async function getStaticPaths() {
const pageSize = 2;
const allArticles = await data.articles.find();
const pageCount = Math.ceil(allArticles.length / pageSize);
const pageSize = 2
const allArticles = await data.articles.find()
const pageCount = Math.ceil(allArticles.length / pageSize)
const pages = range(0, pageCount).map((index) => {
const start = index * pageSize;
const end = start + pageSize;
const start = index * pageSize
const end = start + pageSize
return {
pageNumber: index + 1,
pageCount,
pageSize,
articles: allArticles.slice(start, end),
};
});
articles: allArticles.slice(start, end)
}
})
return pages.map((page) => ({
params: { page: String(page.pageNumber) },
props: page,
}));
props: page
}))
}
const { props } = Astro;
const { props } = Astro
---
<Articles {...props} />

View File

@@ -1,17 +1,17 @@
import { data } from '@/data/data.ts';
import rss from '@astrojs/rss';
import type { APIContext } from 'astro';
import { data } from '@/data/data.ts'
import rss from '@astrojs/rss'
import type { APIContext } from 'astro'
export async function GET(context: APIContext) {
const articles = await data.articles.find();
const profile = data.profile;
const articles = await data.articles.find()
const profile = data.profile
return rss({
title: profile.basics.name,
description: profile.basics.tagline,
site: context.site || 'http://localhost:3000',
items: articles.map((article) => ({
...article.data,
link: `/articles/${article.slug}/`,
})),
});
link: `/articles/${article.slug}/`
}))
})
}

View File

@@ -1,6 +1,6 @@
---
import {} from '@/data/data.js';
import Frontpage from '@/layouts/frontpage/frontpage.astro';
import {} from '@/data/data.js'
import Frontpage from '@/layouts/frontpage/frontpage.astro'
---
<Frontpage />

View File

@@ -1,13 +1,13 @@
import { icons } from '@/assets/images/icons.js';
import { data } from '@/data/data.js';
import type { ManifestOptions } from 'vite-plugin-pwa';
import { icons } from '@/assets/images/icons.js'
import { data } from '@/data/data.js'
import type { ManifestOptions } from 'vite-plugin-pwa'
export async function GET() {
const [maskableIcon] = icons.pngs.filter(
(icon) => icon.size === '512x512' && icon.src.includes('png'),
);
const nonMaskableIcons = icons.pngs.filter((icon) => icon !== maskableIcon);
const basics = data.profile.basics;
(icon) => icon.size === '512x512' && icon.src.includes('png')
)
const nonMaskableIcons = icons.pngs.filter((icon) => icon !== maskableIcon)
const basics = data.profile.basics
const manifest: Partial<ManifestOptions> = {
name: basics.name,
@@ -21,7 +21,7 @@ export async function GET() {
...nonMaskableIcons.map((png) => ({
src: png.src,
sizes: png.size,
type: 'image/png',
type: 'image/png'
})),
...(maskableIcon
? [
@@ -29,11 +29,11 @@ export async function GET() {
src: maskableIcon.src,
sizes: maskableIcon.size,
type: 'image/png',
purpose: 'maskable',
},
purpose: 'maskable'
}
]
: []),
],
};
return new Response(JSON.stringify(manifest, null, 2));
: [])
]
}
return new Response(JSON.stringify(manifest, null, 2))
}

View File

@@ -1,6 +1,6 @@
import { data } from '@/data/data.ts';
import { data } from '@/data/data.ts'
export async function GET() {
const resume = await data.getJsonResume();
return new Response(JSON.stringify(resume, null, 2));
const resume = await data.getJsonResume()
return new Response(JSON.stringify(resume, null, 2))
}

View File

@@ -1,5 +1,5 @@
---
import Work from '@/layouts/work-history/work-history.astro';
import Work from '@/layouts/work-history/work-history.astro'
---
<Work />

View File

@@ -1,6 +1,6 @@
:root {
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu,
Cantarell, 'Helvetica Neue', sans-serif;
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
--font-family-heading: var(--font-family);
--font-size: 15px;
--letter-spacing: 0.5px;

View File

@@ -1,58 +1,58 @@
/**
* Similar to the standard date type, but each section after the year is optional. e.g. 2014-06-29 or 2023-04
*/
export type Iso8601 = string;
export type Iso8601 = string
export interface ResumeSchema {
/**
* link to the version of the schema that can validate the resume
*/
$schema?: string;
$schema?: string
basics?: {
name?: string;
name?: string
/**
* e.g. Web Developer
*/
label?: string;
label?: string
/**
* URL (as per RFC 3986) to a image in JPEG or PNG format
*/
image?: string;
image?: string
/**
* e.g. thomas@gmail.com
*/
email?: string;
email?: string
/**
* Phone numbers are stored as strings so use any format you like, e.g. 712-117-2923
*/
phone?: string;
phone?: string
/**
* URL (as per RFC 3986) to your website, e.g. personal homepage
*/
url?: string;
url?: string
/**
* Write a short 2-3 sentence biography about yourself
*/
summary?: string;
summary?: string
location?: {
/**
* To add multiple address lines, use
* . For example, 1234 Glücklichkeit Straße
* Hinterhaus 5. Etage li.
*/
address?: string;
postalCode?: string;
city?: string;
address?: string
postalCode?: string
city?: string
/**
* code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN
*/
countryCode?: string;
countryCode?: string
/**
* The general region where you live. Can be a US state, or a province, for instance.
*/
region?: string;
[k: string]: unknown;
};
region?: string
[k: string]: unknown
}
/**
* Specify any number of social networks that you participate in
*/
@@ -60,106 +60,106 @@ export interface ResumeSchema {
/**
* e.g. Facebook or Twitter
*/
network?: string;
network?: string
/**
* e.g. neutralthoughts
*/
username?: string;
username?: string
/**
* e.g. http://twitter.example.com/neutralthoughts
*/
url?: string;
[k: string]: unknown;
}[];
[k: string]: unknown;
};
url?: string
[k: string]: unknown
}[]
[k: string]: unknown
}
work?: {
/**
* e.g. Facebook
*/
name?: string;
name?: string
/**
* e.g. Menlo Park, CA
*/
location?: string;
location?: string
/**
* e.g. Social Media Company
*/
description?: string;
description?: string
/**
* e.g. Software Engineer
*/
position?: string;
position?: string
/**
* e.g. http://facebook.example.com
*/
url?: string;
startDate?: Iso8601;
endDate?: Iso8601;
url?: string
startDate?: Iso8601
endDate?: Iso8601
/**
* Give an overview of your responsibilities at the company
*/
summary?: string;
summary?: string
/**
* Specify multiple accomplishments
*/
highlights?: string[];
[k: string]: unknown;
}[];
highlights?: string[]
[k: string]: unknown
}[]
volunteer?: {
/**
* e.g. Facebook
*/
organization?: string;
organization?: string
/**
* e.g. Software Engineer
*/
position?: string;
position?: string
/**
* e.g. http://facebook.example.com
*/
url?: string;
startDate?: Iso8601;
endDate?: Iso8601;
url?: string
startDate?: Iso8601
endDate?: Iso8601
/**
* Give an overview of your responsibilities at the company
*/
summary?: string;
summary?: string
/**
* Specify accomplishments and achievements
*/
highlights?: string[];
[k: string]: unknown;
}[];
highlights?: string[]
[k: string]: unknown
}[]
education?: {
/**
* e.g. Massachusetts Institute of Technology
*/
institution?: string;
institution?: string
/**
* e.g. http://facebook.example.com
*/
url?: string;
url?: string
/**
* e.g. Arts
*/
area?: string;
area?: string
/**
* e.g. Bachelor
*/
studyType?: string;
startDate?: Iso8601;
endDate?: Iso8601;
studyType?: string
startDate?: Iso8601
endDate?: Iso8601
/**
* grade point average, e.g. 3.67/4.0
*/
score?: string;
score?: string
/**
* List notable courses/subjects
*/
courses?: string[];
[k: string]: unknown;
}[];
courses?: string[]
[k: string]: unknown
}[]
/**
* Specify any awards you have received throughout your professional career
*/
@@ -167,18 +167,18 @@ export interface ResumeSchema {
/**
* e.g. One of the 100 greatest minds of the century
*/
title?: string;
date?: Iso8601;
title?: string
date?: Iso8601
/**
* e.g. Time Magazine
*/
awarder?: string;
awarder?: string
/**
* e.g. Received for my work with Quantum Physics
*/
summary?: string;
[k: string]: unknown;
}[];
summary?: string
[k: string]: unknown
}[]
/**
* Specify any certificates you have received throughout your professional career
*/
@@ -186,18 +186,18 @@ export interface ResumeSchema {
/**
* e.g. Certified Kubernetes Administrator
*/
name?: string;
date?: Iso8601;
name?: string
date?: Iso8601
/**
* e.g. http://example.com
*/
url?: string;
url?: string
/**
* e.g. CNCF
*/
issuer?: string;
[k: string]: unknown;
}[];
issuer?: string
[k: string]: unknown
}[]
/**
* Specify your publications through your career
*/
@@ -205,22 +205,22 @@ export interface ResumeSchema {
/**
* e.g. The World Wide Web
*/
name?: string;
name?: string
/**
* e.g. IEEE, Computer Magazine
*/
publisher?: string;
releaseDate?: Iso8601;
publisher?: string
releaseDate?: Iso8601
/**
* e.g. http://www.computer.org.example.com/csdl/mags/co/1996/10/rx069-abs.html
*/
url?: string;
url?: string
/**
* Short summary of publication. e.g. Discussion of the World Wide Web, HTTP, HTML.
*/
summary?: string;
[k: string]: unknown;
}[];
summary?: string
[k: string]: unknown
}[]
/**
* List out your professional skill-set
*/
@@ -228,17 +228,17 @@ export interface ResumeSchema {
/**
* e.g. Web Development
*/
name?: string;
name?: string
/**
* e.g. Master
*/
level?: string;
level?: string
/**
* List some keywords pertaining to this skill
*/
keywords?: string[];
[k: string]: unknown;
}[];
keywords?: string[]
[k: string]: unknown
}[]
/**
* List any other languages you speak
*/
@@ -246,21 +246,21 @@ export interface ResumeSchema {
/**
* e.g. English, Spanish
*/
language?: string;
language?: string
/**
* e.g. Fluent, Beginner
*/
fluency?: string;
[k: string]: unknown;
}[];
fluency?: string
[k: string]: unknown
}[]
interests?: {
/**
* e.g. Philosophy
*/
name?: string;
keywords?: string[];
[k: string]: unknown;
}[];
name?: string
keywords?: string[]
[k: string]: unknown
}[]
/**
* List references you have received
*/
@@ -268,13 +268,13 @@ export interface ResumeSchema {
/**
* e.g. Timothy Cook
*/
name?: string;
name?: string
/**
* e.g. Joe blogs was a great employee, who turned up to work at least once a week. He exceeded my expectations when it came to doing nothing.
*/
reference?: string;
[k: string]: unknown;
}[];
reference?: string
[k: string]: unknown
}[]
/**
* Specify career projects
*/
@@ -282,39 +282,39 @@ export interface ResumeSchema {
/**
* e.g. The World Wide Web
*/
name?: string;
name?: string
/**
* Short summary of project. e.g. Collated works of 2017.
*/
description?: string;
description?: string
/**
* Specify multiple features
*/
highlights?: string[];
highlights?: string[]
/**
* Specify special elements involved
*/
keywords?: string[];
startDate?: Iso8601;
endDate?: Iso8601;
keywords?: string[]
startDate?: Iso8601
endDate?: Iso8601
/**
* e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html
*/
url?: string;
url?: string
/**
* Specify your role on this project or in company
*/
roles?: string[];
roles?: string[]
/**
* Specify the relevant company/entity affiliations e.g. 'greenpeace', 'corporationXYZ'
*/
entity?: string;
entity?: string
/**
* e.g. 'volunteering', 'presentation', 'talk', 'application', 'conference'
*/
type?: string;
[k: string]: unknown;
}[];
type?: string
[k: string]: unknown
}[]
/**
* The schema version and any other tooling configuration lives here
*/
@@ -322,15 +322,15 @@ export interface ResumeSchema {
/**
* URL (as per RFC 3986) to latest version of this document
*/
canonical?: string;
canonical?: string
/**
* A version field which follows semver - e.g. v1.0.0
*/
version?: string;
version?: string
/**
* Using ISO 8601 with YYYY-MM-DDThh:mm:ss
*/
lastModified?: string;
[k: string]: unknown;
};
lastModified?: string
[k: string]: unknown
}
}

View File

@@ -1,5 +1,5 @@
const range = (start: number, end: number) => {
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
};
return Array.from({ length: end - start + 1 }, (_, i) => start + i)
}
export { range };
export { range }

View File

@@ -1,12 +1,12 @@
const formatDate: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
day: 'numeric',
};
day: 'numeric'
}
const formatYearMonth: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'short',
};
month: 'short'
}
export { formatDate, formatYearMonth };
export { formatDate, formatYearMonth }