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/ /node_modules/
/.astro/ /.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 name: Deploy Astro site to Pages
on: on:
push: push:
branches: [$default-branch] branches:
- main
workflow_dispatch: workflow_dispatch:
permissions: 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": [] "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 { defineConfig } from 'astro/config'
import mdx from '@astrojs/mdx'; import mdx from '@astrojs/mdx'
import sitemap from '@astrojs/sitemap'; import sitemap from '@astrojs/sitemap'
import icon from "astro-icon"; import icon from 'astro-icon'
import compress from "astro-compress"; import compress from 'astro-compress'
import robotsTxt from 'astro-robots-txt'; import robotsTxt from 'astro-robots-txt'
const getSiteInfo = () => { const getSiteInfo = () => {
const siteUrl = process.env.SITE_URL; const siteUrl = process.env.SITE_URL
if (!siteUrl) { if (!siteUrl) {
return {}; return {}
} }
const url = new URL(siteUrl); const url = new URL(siteUrl)
return { return {
site: `${url.protocol}//${url.host}`, site: `${url.protocol}//${url.host}`,
base: url.pathname base: url.pathname
}; }
}; }
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
@@ -26,4 +26,4 @@ export default defineConfig({
assetsInlineLimit: 1024 * 10 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": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"start": "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", "build": "astro check && astro build",
"preview": "astro preview", "preview": "astro preview",
"serve": "SITE_URL=http://localhost:3000 pnpm build && serve dist", "serve": "SITE_URL=http://localhost:3000 pnpm build && serve dist",
@@ -35,17 +36,14 @@
"astro-icon": "^1.1.0", "astro-icon": "^1.1.0",
"astro-robots-txt": "^1.0.0", "astro-robots-txt": "^1.0.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-astro": "^0.33.1", "eslint-plugin-astro": "^0.33.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0", "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", "husky": "^9.0.11",
"json-schema-to-typescript": "^13.1.2", "json-schema-to-typescript": "^13.1.2",
"less": "^4.2.0", "less": "^4.2.0",
"lint-staged": "^15.2.2", "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", "prettier-plugin-astro": "^0.13.0",
"sass": "^1.72.0", "sass": "^1.72.0",
"serve": "^14.2.1", "serve": "^14.2.1",

209
pnpm-lock.yaml generated
View File

@@ -58,24 +58,12 @@ devDependencies:
eslint: eslint:
specifier: ^8.57.0 specifier: ^8.57.0
version: 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: eslint-plugin-astro:
specifier: ^0.33.1 specifier: ^0.33.1
version: 0.33.1(eslint@8.57.0) 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: eslint-plugin-jsx-a11y:
specifier: ^6.8.0 specifier: ^6.8.0
version: 6.8.0(eslint@8.57.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: husky:
specifier: ^9.0.11 specifier: ^9.0.11
version: 9.0.11 version: 9.0.11
@@ -89,8 +77,11 @@ devDependencies:
specifier: ^15.2.2 specifier: ^15.2.2
version: 15.2.2 version: 15.2.2
prettier: prettier:
specifier: 3.2.5 specifier: ^3.2.5
version: 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: prettier-plugin-astro:
specifier: ^0.13.0 specifier: ^0.13.0
version: 0.13.0 version: 0.13.0
@@ -2328,10 +2319,6 @@ packages:
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
dev: true dev: true
/@types/json5@0.0.29:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true
/@types/jsonld@1.5.13: /@types/jsonld@1.5.13:
resolution: {integrity: sha512-n7fUU6W4kSYK8VQlf/LsE9kddBHPKhODoVOjsZswmve+2qLwBy6naWxs/EiuSZN9NU0N06Ra01FR+j87C62T0A==} resolution: {integrity: sha512-n7fUU6W4kSYK8VQlf/LsE9kddBHPKhODoVOjsZswmve+2qLwBy6naWxs/EiuSZN9NU0N06Ra01FR+j87C62T0A==}
dev: true dev: true
@@ -2725,18 +2712,6 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true 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: /array.prototype.flat@1.3.2:
resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -3583,17 +3558,6 @@ packages:
ms: 2.0.0 ms: 2.0.0
dev: true 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: /debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@@ -3715,13 +3679,6 @@ packages:
/dlv@1.1.3: /dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 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: /doctrine@3.0.0:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@@ -4026,54 +3983,6 @@ packages:
semver: 7.6.0 semver: 7.6.0
dev: true 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): /eslint-plugin-astro@0.33.1(eslint@8.57.0):
resolution: {integrity: sha512-wVyxAf8Ulmljv5qJQLgspWe17LR4hLXcksIENtUlEC3W7rleBVEKXS+hIqzBfCbpkBLZpl1tsYes1AGpYHd13w==} resolution: {integrity: sha512-wVyxAf8Ulmljv5qJQLgspWe17LR4hLXcksIENtUlEC3W7rleBVEKXS+hIqzBfCbpkBLZpl1tsYes1AGpYHd13w==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
@@ -4093,41 +4002,6 @@ packages:
- supports-color - supports-color
dev: true 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): /eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0):
resolution: {integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==} resolution: {integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@@ -4153,35 +4027,6 @@ packages:
object.fromentries: 2.0.8 object.fromentries: 2.0.8
dev: true 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: /eslint-scope@7.2.2:
resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -4411,10 +4256,6 @@ packages:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true dev: true
/fast-diff@1.3.0:
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
dev: true
/fast-fifo@1.3.2: /fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
requiresBuild: true requiresBuild: true
@@ -5461,13 +5302,6 @@ packages:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
dev: true 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: /json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -6620,15 +6454,6 @@ packages:
es-object-atoms: 1.0.0 es-object-atoms: 1.0.0
dev: true 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: /object.values@1.2.0:
resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -6931,11 +6756,12 @@ packages:
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
dev: true dev: true
/prettier-linter-helpers@1.0.0: /prettier-config-standard@7.0.0(prettier@3.2.5):
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} resolution: {integrity: sha512-NgZy4TYupJR6aMMuV/Aqs0ONnVhlFT8PXVkYRskxREq8EUhJHOddVfBxPV6fWpgcASpJSgvvhVLk0CBO5M3Hvw==}
engines: {node: '>=6.0.0'} peerDependencies:
prettier: ^2.6.0 || ^3.0.0
dependencies: dependencies:
fast-diff: 1.3.0 prettier: 3.2.5
dev: true dev: true
/prettier-plugin-astro@0.13.0: /prettier-plugin-astro@0.13.0:
@@ -7890,14 +7716,6 @@ packages:
picocolors: 1.0.0 picocolors: 1.0.0
dev: true 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: /synckit@0.9.0:
resolution: {integrity: sha512-7RnqIMq572L8PeEzKeBINYEJDDxpcH8JEgLwUqBd3TkofhFRbkq4QLR0u+36avGAhCRbk2nnmjcW9SE531hPDg==} resolution: {integrity: sha512-7RnqIMq572L8PeEzKeBINYEJDDxpcH8JEgLwUqBd3TkofhFRbkq4QLR0u+36avGAhCRbk2nnmjcW9SE531hPDg==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
@@ -8057,15 +7875,6 @@ packages:
dependencies: dependencies:
typescript: 5.4.2 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: /tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
requiresBuild: true requiresBuild: true

View File

@@ -1,16 +1,18 @@
import { fileURLToPath} from 'url'; import { fileURLToPath } from 'url'
import { resolve, dirname } from 'path'; import { resolve, dirname } from 'path'
import { writeFile, mkdir } from 'fs/promises'; import { writeFile, mkdir } from 'fs/promises'
import { compile } from 'json-schema-to-typescript'; 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 response = await fetch(
const schema = await response.json(); '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'); const location = resolve(root, 'src/types/resume-schema.ts')
console.log(`Writing to ${location}`); console.log(`Writing to ${location}`)
await mkdir(dirname(location), { recursive: true }); await mkdir(dirname(location), { recursive: true })
await writeFile(location, types); await writeFile(location, types)

View File

@@ -1,7 +1,7 @@
import { getImage } from 'astro:assets'; import { getImage } from 'astro:assets'
import { data } from '@/data/data.js'; 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( const pngs = await Promise.all(
imageSizes.map(async (size) => { imageSizes.map(async (size) => {
@@ -10,14 +10,14 @@ const pngs = await Promise.all(
src: data.profile.image, src: data.profile.image,
format: 'png', format: 'png',
width: size, width: size,
height: size, height: size
})), })),
size: `${size}x${size}`, size: `${size}x${size}`
}; }
}), })
); )
const icons = { const icons = {
pngs, pngs
}; }
export { icons }; 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> <nav>
<a href="/">{basics.name}</a> <a href='/'>{basics.name}</a>
<div>{basics.tagline}</div> <div>{basics.tagline}</div>
</nav> </nav>
<style> <style lang='less'>
nav { nav {
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;

View File

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

View File

@@ -6,11 +6,11 @@ color: '#e7d9ac'
heroImage: ./assets/cover.png heroImage: ./assets/cover.png
--- ---
import { Image } from 'astro:assets'; import { Image } from 'astro:assets'
import TaskBounds from './assets/TaskBounds.png'; import TaskBounds from './assets/TaskBounds.png'
import Frame1 from './assets/Frame1.png'; import Frame1 from './assets/Frame1.png'
import Graph1 from './assets/Graph1.png'; import Graph1 from './assets/Graph1.png'
import Graph2 from './assets/Graph2.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. 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. 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. 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. - If the task is required.
- A priority - 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 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. 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. 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. 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. Bob has four different strategies for finding a plan.

View File

@@ -6,8 +6,8 @@ description: ''
heroImage: ./assets/cover.png heroImage: ./assets/cover.png
--- ---
import graph from './assets/graph.png'; import graph from './assets/graph.png'
import { Image } from 'astro:assets'; 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. 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 ...sorry
<Image src={graph} alt="graph" /> <Image src={graph} alt='graph' />
So this shows our final setup. 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({ const articles = defineCollection({
schema: ({ image }) => schema: ({ image }) =>
@@ -10,10 +10,10 @@ const articles = defineCollection({
updatedDate: z.coerce.date().optional(), updatedDate: z.coerce.date().optional(),
tags: z.array(z.string()).optional(), tags: z.array(z.string()).optional(),
heroImage: image().refine((img) => img.width >= 320, { 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({ const work = defineCollection({
schema: ({ image }) => schema: ({ image }) =>
@@ -26,16 +26,16 @@ const work = defineCollection({
url: z.string().optional(), url: z.string().optional(),
logo: image() logo: image()
.refine((img) => img.width >= 200, { .refine((img) => img.width >= 200, {
message: 'Logo must be at least 320 pixels wide!', message: 'Logo must be at least 320 pixels wide!'
}) })
.optional(), .optional(),
banner: image() banner: image()
.refine((img) => img.height >= 50, { .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({ const references = defineCollection({
schema: () => schema: () =>
@@ -45,16 +45,16 @@ const references = defineCollection({
company: z.string(), company: z.string(),
date: z.coerce.date(), date: z.coerce.date(),
relation: z.string(), relation: z.string(),
profile: z.string(), profile: z.string()
}), })
}); })
const skills = defineCollection({ const skills = defineCollection({
schema: () => schema: () =>
z.object({ z.object({
name: z.string(), 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 type { ResumeSchema } from '@/types/resume-schema.js'
import { Content } from './description.md'; import { Content } from './description.md'
import image from './profile.jpg'; import image from './profile.jpg'
const basics = { const basics = {
name: 'Morten Olsen', name: 'Morten Olsen',
@@ -11,38 +11,38 @@ const basics = {
location: { location: {
city: 'Copenhagen', city: 'Copenhagen',
countryCode: 'DK', countryCode: 'DK',
region: 'Capital Region of Denmark', region: 'Capital Region of Denmark'
}, },
profiles: [ profiles: [
{ {
network: 'GitHub', network: 'GitHub',
icon: 'mdi:github', icon: 'mdi:github',
username: 'morten-olsen', username: 'morten-olsen',
url: 'https://github.com/morten-olsen', url: 'https://github.com/morten-olsen'
}, },
{ {
network: 'LinkedIn', network: 'LinkedIn',
icon: 'mdi:linkedin', icon: 'mdi:linkedin',
username: 'mortenolsendk', username: 'mortenolsendk',
url: 'https://www.linkedin.com/in/mortenolsendk', url: 'https://www.linkedin.com/in/mortenolsendk'
}, }
], ],
languages: [ languages: [
{ {
name: 'English', name: 'English',
fluency: 'Conversational', fluency: 'Conversational'
}, },
{ {
name: 'Danish', name: 'Danish',
fluency: 'Native speaker', fluency: 'Native speaker'
}, }
], ]
} satisfies ResumeSchema['basics']; } satisfies ResumeSchema['basics']
const profile = { const profile = {
basics, basics,
image, 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 { class Articles {
public find = () => getCollection('articles'); public find = () => getCollection('articles')
public get = async (slug: string) => { public get = async (slug: string) => {
const collection = await this.find(); const collection = await this.find()
return collection.find((entry) => entry.slug === slug); 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 { class References {
public find = () => getCollection('references'); public find = () => getCollection('references')
public get = async (slug: string) => { public get = async (slug: string) => {
const collection = await this.find(); const collection = await this.find()
return collection.find((entry) => entry.slug === slug); 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 = { 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 { class Skills {
public find = () => getCollection('skills'); public find = () => getCollection('skills')
public get = async (slug: string) => { public get = async (slug: string) => {
const collection = await this.find(); const collection = await this.find()
return collection.find((entry) => entry.slug === slug); 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 { profile } from '../content/profile/profile.js'
import { type Article, Articles } from './data.articles.js'; import { type Article, Articles } from './data.articles.js'
import { References } from './data.references.ts'; import { References } from './data.references.ts'
import { site } from './data.site.ts'; import { site } from './data.site.ts'
import { Skills } from './data.skills.ts'; import { Skills } from './data.skills.ts'
import { getJsonLDResume, getJsonResume } from './data.utils.js'; import { getJsonLDResume, getJsonResume } from './data.utils.js'
import { Work, type WorkItem } from './data.work.js'; import { Work, type WorkItem } from './data.work.js'
class Data { class Data {
public articles = new Articles(); public articles = new Articles()
public work = new Work(); public work = new Work()
public references = new References(); public references = new References()
public skills = new Skills(); public skills = new Skills()
public profile = profile; public profile = profile
public site = site; public site = site
public getJsonResume = getJsonResume.bind(null, this); public getJsonResume = getJsonResume.bind(null, this)
public getJsonLDResume = getJsonLDResume.bind(null, this); public getJsonLDResume = getJsonLDResume.bind(null, this)
} }
const data = new Data(); const data = new Data()
type Profile = typeof profile; type Profile = typeof profile
export type { Article, Profile, WorkItem }; export type { Article, Profile, WorkItem }
export { data, Data }; 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 getJsonResume = async (data: Data) => {
const profile = data.profile; const profile = data.profile
const resume = { const resume = {
basics: profile.basics, basics: profile.basics
} satisfies ResumeSchema; } satisfies ResumeSchema
return resume; return resume
}; }
const getArticleJsonLD = async (data: Data, article: Article) => { const getArticleJsonLD = async (data: Data, article: Article) => {
const jsonld = { const jsonld = {
@@ -23,16 +23,16 @@ const getArticleJsonLD = async (data: Data, article: Article) => {
author: { author: {
'@type': 'Person', '@type': 'Person',
name: data.profile.basics.name, name: data.profile.basics.name,
url: data.profile.basics.url, url: data.profile.basics.url
}, }
}; }
return jsonld; return jsonld
}; }
const getJsonLDResume = async (data: Data) => { const getJsonLDResume = async (data: Data) => {
const work = await data.work.find(); const work = await data.work.find()
const currentWork = work.find((w) => !w.data.endDate); const currentWork = work.find((w) => !w.data.endDate)
const otherWork = work.filter((w) => w !== currentWork); const otherWork = work.filter((w) => w !== currentWork)
const jsonld = { const jsonld = {
'@context': 'https://schema.org', '@context': 'https://schema.org',
@@ -47,24 +47,24 @@ const getJsonLDResume = async (data: Data) => {
'@type': 'ContactPoint', '@type': 'ContactPoint',
contactType: profile.network.toLowerCase(), contactType: profile.network.toLowerCase(),
identifier: profile.username, identifier: profile.username,
url: profile.url, url: profile.url
})), })),
address: { address: {
'@type': 'PostalAddress', '@type': 'PostalAddress',
addressLocality: data.profile.basics.location.city, addressLocality: data.profile.basics.location.city,
addressRegion: data.profile.basics.location.region, 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), sameAs: data.profile.basics.profiles.map((profile) => profile.url),
hasOccupation: currentWork && { hasOccupation: currentWork && {
'@type': 'EmployeeRole', '@type': 'EmployeeRole',
roleName: currentWork.data.position, roleName: currentWork.data.position,
startDate: currentWork.data.startDate.toISOString(), startDate: currentWork.data.startDate.toISOString()
}, },
worksFor: currentWork && { worksFor: currentWork && {
'@type': 'Organization', '@type': 'Organization',
name: currentWork?.data.name, name: currentWork?.data.name,
sameAs: currentWork?.data.url, sameAs: currentWork?.data.url
}, },
alumniOf: otherWork.map((w) => ({ alumniOf: otherWork.map((w) => ({
'@type': 'Organization', '@type': 'Organization',
@@ -76,14 +76,14 @@ const getJsonLDResume = async (data: Data) => {
'@type': 'EmployeeRole', '@type': 'EmployeeRole',
roleName: w.data.position, roleName: w.data.position,
startDate: w.data.startDate.toISOString(), 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 { class Work {
public find = () => getCollection('work'); public find = () => getCollection('work')
public get = async (slug: string) => { public get = async (slug: string) => {
const collection = await this.find(); const collection = await this.find()
return collection.find((entry) => entry.slug === slug); return collection.find((entry) => entry.slug === slug)
}; }
} }
type WorkItem = Exclude<Awaited<ReturnType<Work['get']>>, undefined>; type WorkItem = Exclude<Awaited<ReturnType<Work['get']>>, undefined>
export { Work, type WorkItem }; 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 { type Article, data } from '@/data/data.js'
import { getArticleJsonLD } from '@/data/data.utils'; import { getArticleJsonLD } from '@/data/data.utils'
import Html from '../html/html.astro'; import Html from '../html/html.astro'
type Props = { type Props = {
article: Article; article: Article
}; }
const { props } = Astro; const { props } = Astro
const { article } = props; const { article } = props
const { Content } = await article.render(); const { Content } = await article.render()
--- ---
<Html <Html
@@ -24,22 +24,22 @@ const { Content } = await article.render();
<h1> <h1>
{article.data.title.split(' ').map((word) => <span>{word}</span>)} {article.data.title.split(' ').map((word) => <span>{word}</span>)}
</h1> </h1>
<a href="/"><h2>By {data.profile.basics.name}</h2></a> <a href='/'><h2>By {data.profile.basics.name}</h2></a>
</header> </header>
<Picture <Picture
loading="eager" loading='eager'
class="img" class='img'
src={article.data.heroImage} src={article.data.heroImage}
widths={[320, 640, 1024, 1400]} widths={[320, 640, 1024, 1400]}
formats={['avif', 'webp', 'png']} formats={['avif', 'webp', 'png']}
alt="Cover image" alt='Cover image'
/> />
<div class="content"> <div class='content'>
<Content /> <Content />
</div> </div>
</article> </article>
</Html> </Html>
<style lang="less"> <style lang='less'>
article { article {
--left-padding: 100px; --left-padding: 100px;
display: grid; display: grid;

View File

@@ -1,20 +1,20 @@
--- ---
import type { Article } from '@/data/data.js'; import type { Article } from '@/data/data.js'
import { range } from '@/utils/data'; import { range } from '@/utils/data'
import Html from '../html/html.astro'; import Html from '../html/html.astro'
type Props = { type Props = {
pageNumber: number; pageNumber: number
pageCount: number; pageCount: number
articles: Article[]; articles: Article[]
}; }
const { articles, pageNumber, pageCount } = Astro.props; const { articles, pageNumber, pageCount } = Astro.props
const hasPrev = pageNumber > 1; const hasPrev = pageNumber > 1
const hasNext = pageNumber < pageCount; const hasNext = pageNumber < pageCount
--- ---
<Html title="Articles" description="A list of articles"> <Html title='Articles' description='A list of articles'>
<h1>Articles</h1> <h1>Articles</h1>
{ {
articles.map((article) => ( articles.map((article) => (
@@ -26,7 +26,9 @@ const hasNext = pageNumber < pageCount;
} }
<nav> <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) => ( range(1, pageCount).map((page) => (
<a <a
@@ -37,11 +39,13 @@ const hasNext = pageNumber < pageCount;
</a> </a>
)) ))
} }
<a aria-disabled={!hasNext} href={`/articles/pages/${pageNumber + 1}`}> Next </a> <a aria-disabled={!hasNext} href={`/articles/pages/${pageNumber + 1}`}>
Next
</a>
</nav> </nav>
</Html> </Html>
<style> <style lang='less'>
nav { nav {
display: flex; display: flex;
justify-content: center; 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 = { type Props = {
class?: string; class?: string
}; }
const { class: className, ...rest } = Astro.props; const { class: className, ...rest } = Astro.props
const articleCount = 6; const articleCount = 6
const allArticles = await data.articles.find(); const allArticles = await data.articles.find()
const sortedArticles = allArticles.sort( const sortedArticles = allArticles.sort(
(a, b) => new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime(), (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); const hasMore = sortedArticles.length > articleCount
const articles = sortedArticles.slice(0, articleCount)
--- ---
<div class:list={['articles', className]} {...rest}> <div class:list={['articles', className]} {...rest}>
<h2>Articles</h2> <h2>Articles</h2>
<div class="items"> <div class='items'>
{articles.map((article) => <Article article={article} />)} {articles.map((article) => <Article article={article} />)}
</div> </div>
{hasMore && <a href="/articles/pages/1">View all articles</a>} {hasMore && <a href='/articles/pages/1'>View all articles</a>}
</div> </div>
<style> <style lang='less'>
.articles { .articles {
display: grid; display: grid;
gap: var(--space-lg); 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 Time from '@/components/time/absolute.astro'
import type { Article } from '@/data/data.js'; import type { Article } from '@/data/data.js'
import { formatDate } from '@/utils/time.js'; import { formatDate } from '@/utils/time.js'
type Props = { type Props = {
article: Article; article: Article
}; }
const { article: item } = Astro.props; const { article: item } = Astro.props
--- ---
<a href={`/articles/${item.slug}`}> <a href={`/articles/${item.slug}`}>
<article> <article>
<Picture <Picture
class="thumb" class='thumb'
alt="thumbnail image" alt='thumbnail image'
src={item.data.heroImage} src={item.data.heroImage}
formats={['avif', 'webp', 'jpeg']} formats={['avif', 'webp', 'jpeg']}
width={100} width={100}
/> />
<div class="content"> <div class='content'>
<small> <small>
<Time format={formatDate} datetime={item.data.pubDate} /> <Time format={formatDate} datetime={item.data.pubDate} />
</small> </small>
@@ -30,7 +30,7 @@ const { article: item } = Astro.props;
</article> </article>
</a> </a>
<style> <style lang='less'>
a { a {
width: 45%; 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 = { type Props = {
class?: string; class?: string
}; }
const { class: className, ...rest } = Astro.props; const { class: className, ...rest } = Astro.props
const { Content, basics, image } = data.profile; const { Content, basics, image } = data.profile
--- ---
<div class:list={['main', className]} {...rest}> <div class:list={['main', className]} {...rest}>
<Picture <Picture
class="picture" class='picture'
alt="Profile Picture" alt='Profile Picture'
src={image} src={image}
formats={['avif', 'webp', 'jpeg']} formats={['avif', 'webp', 'jpeg']}
width={230} width={230}
/> />
<h1>{basics.name}</h1> <h1>{basics.name}</h1>
<h2>{basics.tagline}</h2> <h2>{basics.tagline}</h2>
<div class="description"> <div class='description'>
<Content /> <Content />
</div> </div>
<div class="profiles"> <div class='profiles'>
{basics.profiles.map((profile) => <Profile profile={profile} />)} {basics.profiles.map((profile) => <Profile profile={profile} />)}
</div> </div>
</div> </div>
<style> <style lang='less'>
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.main { .main {
display: flex; 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 = { type Props = {
profile: Profile['basics']['profiles'][number]; profile: Profile['basics']['profiles'][number]
}; }
const { profile } = Astro.props; const { profile } = Astro.props
--- ---
<a href={profile.url} target="_blank"> <a href={profile.url} target='_blank'>
<Icon class="icon" name={profile.icon} /> <Icon class='icon' name={profile.icon} />
<div class="network">{profile.network}</div> <div class='network'>{profile.network}</div>
<div class="username">{profile.username}</div> <div class='username'>{profile.username}</div>
</a> </a>
<style> <style lang='less'>
a { a {
display: grid; display: grid;
align-items: center; 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 Html from '../html/html.astro'
import Articles from './articles/articles.astro'; import Articles from './articles/articles.astro'
import Description from './description/description.astro'; import Description from './description/description.astro'
import Info from './info/info.astro'; import Info from './info/info.astro'
import Skills from './skills/skills.astro'; import Skills from './skills/skills.astro'
import Work from './work/work.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}> <Html
<div class="wrapper"> title={data.profile.basics.name}
<div class="frontpage"> description='Landing page'
<Description class="description" /> jsonLd={jsonLd}
<Info class="info" /> >
<Articles class="articles" /> <div class='wrapper'>
<Skills class="skills" /> <div class='frontpage'>
<Work class="work" /> <Description class='description' />
<Info class='info' />
<Articles class='articles' />
<Skills class='skills' />
<Work class='work' />
</div> </div>
</div> </div>
</Html> </Html>
<style> <style lang='less'>
.wrapper { .wrapper {
--gap: var(--space-xxl); --gap: var(--space-xxl);
margin: 0 auto; margin: 0 auto;

View File

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

View File

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

View File

@@ -1,31 +1,33 @@
--- ---
import Time from '@/components/time/absolute.astro'; import Time from '@/components/time/absolute.astro'
import { data } from '@/data/data.js'; import { data } from '@/data/data.js'
import { formatYearMonth } from '@/utils/time.js'; import { formatYearMonth } from '@/utils/time.js'
type Props = { type Props = {
class?: string; class?: string
}; }
const { class: className, ...rest } = Astro.props; const { class: className, ...rest } = Astro.props
const allWork = await data.work.find(); const allWork = await data.work.find()
const work = allWork.sort((a, b) => { 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}> <div class:list={[className]} {...rest}>
<h2>Work</h2> <h2>Work</h2>
<div class="list"> <div class='list'>
{ {
work.map((item) => ( work.map((item) => (
<div class="item"> <div class='item'>
<div class="time"> <div class='time'>
<Time format={formatYearMonth} datetime={item.data.endDate} /> <Time format={formatYearMonth} datetime={item.data.endDate} />
<br /> <br />
<Time format={formatYearMonth} datetime={item.data.startDate} /> <Time format={formatYearMonth} datetime={item.data.startDate} />
</div> </div>
<div class="content"> <div class='content'>
<h3>{item.data.position}</h3> <h3>{item.data.position}</h3>
<h4>@ {item.data.name}</h4> <h4>@ {item.data.name}</h4>
<p>{item.data.summary}</p> <p>{item.data.summary}</p>
@@ -34,10 +36,10 @@ const work = allWork.sort((a, b) => {
)) ))
} }
</div> </div>
<a href="/work-history">See full work history</a> <a href='/work-history'>See full work history</a>
</div> </div>
<style> <style lang='less'>
h2 { h2 {
font-size: var(--font-xl); font-size: var(--font-xl);
margin-bottom: var(--space-lg); 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 { icons } from '@/assets/images/icons.js'
import { data } from '@/data/data.js'; import { data } from '@/data/data.js'
type Props = { type Props = {
title: string; title: string
description: string; description: string
image?: string; image?: string
jsonLd?: unknown; jsonLd?: unknown
}; }
const { props } = Astro; const { props } = Astro
const schema = JSON.stringify(props.jsonLd); const schema = JSON.stringify(props.jsonLd)
--- ---
<!doctype html> <!doctype html>
<html lang="en"> <html lang='en'>
<Head> <Head>
<meta charset="UTF-8" /> <meta charset='UTF-8' />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv='X-UA-Compatible' content='IE=edge' />
<meta name="HandheldFriendly" content="True" /> <meta name='HandheldFriendly' content='True' />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>{props.title}</title> <title>{props.title}</title>
<link rel="sitemap" href="/sitemap-index.xml" /> <link rel='sitemap' href='/sitemap-index.xml' />
<link rel="manifest" href="/manifest.webmanifest" /> <link rel='manifest' href='/manifest.webmanifest' />
<meta name="generator" content={Astro.generator} /> <meta name='generator' content={Astro.generator} />
<meta name="theme-color" content={data.site.theme} /> <meta name='theme-color' content={data.site.theme} />
<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="/articles/rss.xml" /> <link
{props.description && <meta name="description" content={props.description} />} rel='alternate'
{props.image && <meta property="og:image" content={props.image} />} type='application/rss+xml'
{props.jsonLd && <script type="application/ld+json" is:inline set:html={schema} />} 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) => ( 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> </Head>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
:root { :root {
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Cantarell, 'Helvetica Neue', sans-serif; Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
--font-family-heading: var(--font-family); --font-family-heading: var(--font-family);
--font-size: 15px; --font-size: 15px;
--letter-spacing: 0.5px; --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 * 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 { export interface ResumeSchema {
/** /**
* link to the version of the schema that can validate the resume * link to the version of the schema that can validate the resume
*/ */
$schema?: string; $schema?: string
basics?: { basics?: {
name?: string; name?: string
/** /**
* e.g. Web Developer * e.g. Web Developer
*/ */
label?: string; label?: string
/** /**
* URL (as per RFC 3986) to a image in JPEG or PNG format * URL (as per RFC 3986) to a image in JPEG or PNG format
*/ */
image?: string; image?: string
/** /**
* e.g. thomas@gmail.com * 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 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 (as per RFC 3986) to your website, e.g. personal homepage
*/ */
url?: string; url?: string
/** /**
* Write a short 2-3 sentence biography about yourself * Write a short 2-3 sentence biography about yourself
*/ */
summary?: string; summary?: string
location?: { location?: {
/** /**
* To add multiple address lines, use * To add multiple address lines, use
* . For example, 1234 Glücklichkeit Straße * . For example, 1234 Glücklichkeit Straße
* Hinterhaus 5. Etage li. * Hinterhaus 5. Etage li.
*/ */
address?: string; address?: string
postalCode?: string; postalCode?: string
city?: string; city?: string
/** /**
* code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN * 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. * The general region where you live. Can be a US state, or a province, for instance.
*/ */
region?: string; region?: string
[k: string]: unknown; [k: string]: unknown
}; }
/** /**
* Specify any number of social networks that you participate in * Specify any number of social networks that you participate in
*/ */
@@ -60,106 +60,106 @@ export interface ResumeSchema {
/** /**
* e.g. Facebook or Twitter * e.g. Facebook or Twitter
*/ */
network?: string; network?: string
/** /**
* e.g. neutralthoughts * e.g. neutralthoughts
*/ */
username?: string; username?: string
/** /**
* e.g. http://twitter.example.com/neutralthoughts * e.g. http://twitter.example.com/neutralthoughts
*/ */
url?: string; url?: string
[k: string]: unknown; [k: string]: unknown
}[]; }[]
[k: string]: unknown; [k: string]: unknown
}; }
work?: { work?: {
/** /**
* e.g. Facebook * e.g. Facebook
*/ */
name?: string; name?: string
/** /**
* e.g. Menlo Park, CA * e.g. Menlo Park, CA
*/ */
location?: string; location?: string
/** /**
* e.g. Social Media Company * e.g. Social Media Company
*/ */
description?: string; description?: string
/** /**
* e.g. Software Engineer * e.g. Software Engineer
*/ */
position?: string; position?: string
/** /**
* e.g. http://facebook.example.com * e.g. http://facebook.example.com
*/ */
url?: string; url?: string
startDate?: Iso8601; startDate?: Iso8601
endDate?: Iso8601; endDate?: Iso8601
/** /**
* Give an overview of your responsibilities at the company * Give an overview of your responsibilities at the company
*/ */
summary?: string; summary?: string
/** /**
* Specify multiple accomplishments * Specify multiple accomplishments
*/ */
highlights?: string[]; highlights?: string[]
[k: string]: unknown; [k: string]: unknown
}[]; }[]
volunteer?: { volunteer?: {
/** /**
* e.g. Facebook * e.g. Facebook
*/ */
organization?: string; organization?: string
/** /**
* e.g. Software Engineer * e.g. Software Engineer
*/ */
position?: string; position?: string
/** /**
* e.g. http://facebook.example.com * e.g. http://facebook.example.com
*/ */
url?: string; url?: string
startDate?: Iso8601; startDate?: Iso8601
endDate?: Iso8601; endDate?: Iso8601
/** /**
* Give an overview of your responsibilities at the company * Give an overview of your responsibilities at the company
*/ */
summary?: string; summary?: string
/** /**
* Specify accomplishments and achievements * Specify accomplishments and achievements
*/ */
highlights?: string[]; highlights?: string[]
[k: string]: unknown; [k: string]: unknown
}[]; }[]
education?: { education?: {
/** /**
* e.g. Massachusetts Institute of Technology * e.g. Massachusetts Institute of Technology
*/ */
institution?: string; institution?: string
/** /**
* e.g. http://facebook.example.com * e.g. http://facebook.example.com
*/ */
url?: string; url?: string
/** /**
* e.g. Arts * e.g. Arts
*/ */
area?: string; area?: string
/** /**
* e.g. Bachelor * e.g. Bachelor
*/ */
studyType?: string; studyType?: string
startDate?: Iso8601; startDate?: Iso8601
endDate?: Iso8601; endDate?: Iso8601
/** /**
* grade point average, e.g. 3.67/4.0 * grade point average, e.g. 3.67/4.0
*/ */
score?: string; score?: string
/** /**
* List notable courses/subjects * List notable courses/subjects
*/ */
courses?: string[]; courses?: string[]
[k: string]: unknown; [k: string]: unknown
}[]; }[]
/** /**
* Specify any awards you have received throughout your professional career * 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 * e.g. One of the 100 greatest minds of the century
*/ */
title?: string; title?: string
date?: Iso8601; date?: Iso8601
/** /**
* e.g. Time Magazine * e.g. Time Magazine
*/ */
awarder?: string; awarder?: string
/** /**
* e.g. Received for my work with Quantum Physics * e.g. Received for my work with Quantum Physics
*/ */
summary?: string; summary?: string
[k: string]: unknown; [k: string]: unknown
}[]; }[]
/** /**
* Specify any certificates you have received throughout your professional career * Specify any certificates you have received throughout your professional career
*/ */
@@ -186,18 +186,18 @@ export interface ResumeSchema {
/** /**
* e.g. Certified Kubernetes Administrator * e.g. Certified Kubernetes Administrator
*/ */
name?: string; name?: string
date?: Iso8601; date?: Iso8601
/** /**
* e.g. http://example.com * e.g. http://example.com
*/ */
url?: string; url?: string
/** /**
* e.g. CNCF * e.g. CNCF
*/ */
issuer?: string; issuer?: string
[k: string]: unknown; [k: string]: unknown
}[]; }[]
/** /**
* Specify your publications through your career * Specify your publications through your career
*/ */
@@ -205,22 +205,22 @@ export interface ResumeSchema {
/** /**
* e.g. The World Wide Web * e.g. The World Wide Web
*/ */
name?: string; name?: string
/** /**
* e.g. IEEE, Computer Magazine * e.g. IEEE, Computer Magazine
*/ */
publisher?: string; publisher?: string
releaseDate?: Iso8601; releaseDate?: Iso8601
/** /**
* e.g. http://www.computer.org.example.com/csdl/mags/co/1996/10/rx069-abs.html * 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. * Short summary of publication. e.g. Discussion of the World Wide Web, HTTP, HTML.
*/ */
summary?: string; summary?: string
[k: string]: unknown; [k: string]: unknown
}[]; }[]
/** /**
* List out your professional skill-set * List out your professional skill-set
*/ */
@@ -228,17 +228,17 @@ export interface ResumeSchema {
/** /**
* e.g. Web Development * e.g. Web Development
*/ */
name?: string; name?: string
/** /**
* e.g. Master * e.g. Master
*/ */
level?: string; level?: string
/** /**
* List some keywords pertaining to this skill * List some keywords pertaining to this skill
*/ */
keywords?: string[]; keywords?: string[]
[k: string]: unknown; [k: string]: unknown
}[]; }[]
/** /**
* List any other languages you speak * List any other languages you speak
*/ */
@@ -246,21 +246,21 @@ export interface ResumeSchema {
/** /**
* e.g. English, Spanish * e.g. English, Spanish
*/ */
language?: string; language?: string
/** /**
* e.g. Fluent, Beginner * e.g. Fluent, Beginner
*/ */
fluency?: string; fluency?: string
[k: string]: unknown; [k: string]: unknown
}[]; }[]
interests?: { interests?: {
/** /**
* e.g. Philosophy * e.g. Philosophy
*/ */
name?: string; name?: string
keywords?: string[]; keywords?: string[]
[k: string]: unknown; [k: string]: unknown
}[]; }[]
/** /**
* List references you have received * List references you have received
*/ */
@@ -268,13 +268,13 @@ export interface ResumeSchema {
/** /**
* e.g. Timothy Cook * 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. * 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; reference?: string
[k: string]: unknown; [k: string]: unknown
}[]; }[]
/** /**
* Specify career projects * Specify career projects
*/ */
@@ -282,39 +282,39 @@ export interface ResumeSchema {
/** /**
* e.g. The World Wide Web * e.g. The World Wide Web
*/ */
name?: string; name?: string
/** /**
* Short summary of project. e.g. Collated works of 2017. * Short summary of project. e.g. Collated works of 2017.
*/ */
description?: string; description?: string
/** /**
* Specify multiple features * Specify multiple features
*/ */
highlights?: string[]; highlights?: string[]
/** /**
* Specify special elements involved * Specify special elements involved
*/ */
keywords?: string[]; keywords?: string[]
startDate?: Iso8601; startDate?: Iso8601
endDate?: Iso8601; endDate?: Iso8601
/** /**
* e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html * 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 * Specify your role on this project or in company
*/ */
roles?: string[]; roles?: string[]
/** /**
* Specify the relevant company/entity affiliations e.g. 'greenpeace', 'corporationXYZ' * Specify the relevant company/entity affiliations e.g. 'greenpeace', 'corporationXYZ'
*/ */
entity?: string; entity?: string
/** /**
* e.g. 'volunteering', 'presentation', 'talk', 'application', 'conference' * e.g. 'volunteering', 'presentation', 'talk', 'application', 'conference'
*/ */
type?: string; type?: string
[k: string]: unknown; [k: string]: unknown
}[]; }[]
/** /**
* The schema version and any other tooling configuration lives here * 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 * 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 * A version field which follows semver - e.g. v1.0.0
*/ */
version?: string; version?: string
/** /**
* Using ISO 8601 with YYYY-MM-DDThh:mm:ss * Using ISO 8601 with YYYY-MM-DDThh:mm:ss
*/ */
lastModified?: string; lastModified?: string
[k: string]: unknown; [k: string]: unknown
}; }
} }

View File

@@ -1,5 +1,5 @@
const range = (start: number, end: number) => { 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 = { const formatDate: Intl.DateTimeFormatOptions = {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
day: 'numeric', day: 'numeric'
}; }
const formatYearMonth: Intl.DateTimeFormatOptions = { const formatYearMonth: Intl.DateTimeFormatOptions = {
year: 'numeric', year: 'numeric',
month: 'short', month: 'short'
}; }
export { formatDate, formatYearMonth }; export { formatDate, formatYearMonth }