Compare commits
67 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8a38b60e3 | |||
| 1ae9325ace | |||
| 73ecdbe4d8 | |||
| bd65162c70 | |||
| fb93eb7574 | |||
| ed4b091cf1 | |||
| 778328d984 | |||
| 44e6fe6722 | |||
| 271a92c7b3 | |||
| d79e51f1ce | |||
| cdc9d091e3 | |||
| ef5c186345 | |||
| 8c2e355bfb | |||
| a9dc107176 | |||
| ec7442da30 | |||
| 7a674694a7 | |||
| d8768998e2 | |||
| 2f39f3d62d | |||
| 28a57aca9a | |||
| e77351e5dd | |||
| fd1d015a18 | |||
| f2bbfbf1b4 | |||
| 4860085aef | |||
| e3be0e92cb | |||
| 1dbd3d662c | |||
| 199f3d1429 | |||
| 74a3a71090 | |||
| 7966746711 | |||
| 7e0380a324 | |||
| 7921721425 | |||
| 5fa6e68b37 | |||
| a71eb0f975 | |||
| 087403f70f | |||
| 25a83048d5 | |||
| 8f09af3829 | |||
| 7ff435d619 | |||
| 9d21f0282b | |||
| 5cc77e537b | |||
| 05dd11db99 | |||
| 2e4cf74810 | |||
| 5140c1fe55 | |||
| d9abb4bd53 | |||
| 5104dc3176 | |||
| 5d2802aca7 | |||
| f0bb8f9272 | |||
| e4081c42e4 | |||
| 9e4eb6ef15 | |||
| a6f7c1153c | |||
| cea29e7a01 | |||
| eaecdbcf23 | |||
| 9d69a6cc67 | |||
| 9b4312b49d | |||
| bb335badf6 | |||
| 85f9b148de | |||
| ca81b780ce | |||
| 0f28734a2d | |||
| df5b89e5b1 | |||
| 8861d7f73d | |||
| 42112a8a9f | |||
| 08ef1cd749 | |||
| 79c31a77d3 | |||
| 4e6554e015 | |||
| 88c159c33b | |||
| ec6e450205 | |||
| eaad49a9be | |||
| 956c4b90e2 | |||
| 1598336ed2 |
20 changed files with 1213 additions and 361 deletions
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
|
|
@ -1,4 +1,8 @@
|
||||||
{
|
{
|
||||||
"recommendations": ["astro-build.astro-vscode"],
|
"recommendations": [
|
||||||
|
"astro-build.astro-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
],
|
||||||
"unwantedRecommendations": []
|
"unwantedRecommendations": []
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
README.md
30
README.md
|
|
@ -22,12 +22,30 @@ $ pnpm i
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
| Command | Action |
|
| Command | Action |
|
||||||
| ------------------- | ---------------------------------------------- |
|
| ----------------------------- | ---------------------------------------------- |
|
||||||
| `pnpm run dev` | start dev server |
|
| `pnpm run dev` | start dev server |
|
||||||
| `pnpm run build` | generate static files to `dist` directory |
|
| `pnpm run build` | generate static files to `dist` directory |
|
||||||
| `pnpm run deploy` | upload to my server |
|
| `pnpm run deploy` | upload to my server |
|
||||||
| `pnpm run prep:now` | back up and clear current `now page` constants |
|
| `pnpm run prep:now` | back up and clear current `now page` constants |
|
||||||
|
| `pnpm run patch:build:deploy` | increment version, build, then upload |
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
This project uses Vitest for unit tests. The test script is defined in `package.json` as `vitest run .`.
|
||||||
|
|
||||||
|
Run tests locally with pnpm (recommended):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# install dependencies (if you haven't already)
|
||||||
|
pnpm i
|
||||||
|
|
||||||
|
# run tests once
|
||||||
|
pnpm test
|
||||||
|
|
||||||
|
# run Vitest in interactive/watch mode
|
||||||
|
pnpm exec vitest
|
||||||
|
```
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { defineConfig } from 'astro/config'
|
import { defineConfig } from 'astro/config'
|
||||||
import serviceWorker from '@ayco/astro-sw'
|
import serviceWorker from '@ayco/astro-sw'
|
||||||
import sitemap from '@astrojs/sitemap'
|
import sitemap from '@astrojs/sitemap'
|
||||||
|
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||||
|
|
||||||
import * as data from './package.json'
|
import * as data from './package.json'
|
||||||
|
|
||||||
|
|
@ -21,6 +22,10 @@ export default defineConfig({
|
||||||
assetCachePrefix: 'ayco-personal-site',
|
assetCachePrefix: 'ayco-personal-site',
|
||||||
assetCacheVersionID: data.version,
|
assetCacheVersionID: data.version,
|
||||||
logAssets: true,
|
logAssets: true,
|
||||||
|
include: [
|
||||||
|
'/wc/node_modules/web-component-base/dist/index.js',
|
||||||
|
'/wc/node_modules/@ayo-run/status-indicator/dist/status-indicator.js',
|
||||||
|
],
|
||||||
esbuild: {
|
esbuild: {
|
||||||
minify: true,
|
minify: true,
|
||||||
},
|
},
|
||||||
|
|
@ -38,4 +43,20 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
vite: {
|
||||||
|
plugins: [
|
||||||
|
viteStaticCopy({
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
src: './node_modules/web-component-base/dist/index.js',
|
||||||
|
dest: 'wc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: './node_modules/@ayo-run/status-indicator/dist/status-indicator.js',
|
||||||
|
dest: 'wc',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import tseslint from 'typescript-eslint'
|
||||||
import astroSwGlobals from '@ayco/astro-sw/globals'
|
import astroSwGlobals from '@ayco/astro-sw/globals'
|
||||||
import astroParser from 'astro-eslint-parser'
|
import astroParser from 'astro-eslint-parser'
|
||||||
|
|
||||||
import { includeIgnoreFile } from '@eslint/compat'
|
import { includeIgnoreFile } from '@eslint/config-helpers'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
|
|
||||||
46
package.json
46
package.json
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@ayco/personal-website",
|
"name": "@ayco/personal-website",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.3.63",
|
"version": "1.3.88",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
|
|
@ -11,56 +11,68 @@
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"lint": "eslint . --config eslint.config.mjs --cache",
|
"lint": "eslint . --config eslint.config.mjs --cache",
|
||||||
"lint:fix": "eslint . --config eslint.config.mjs --fix",
|
"lint:fix": "eslint . --config eslint.config.mjs --fix",
|
||||||
"format": "prettier . --write",
|
"fmt": "prettier . --config prettier.config.mjs --check",
|
||||||
"check": "npm run format && npm run lint",
|
"fmt:fix": "prettier . --config prettier.config.mjs --write",
|
||||||
|
"check": "npm run fmt && npm run lint",
|
||||||
"deploy": "eval $(grep '^HOST' .env) && scp -r dist ayo@$HOST:~/ayco.io-flask",
|
"deploy": "eval $(grep '^HOST' .env) && scp -r dist ayo@$HOST:~/ayco.io-flask",
|
||||||
"patch:build:deploy": "npm version patch && npm run build && npm run deploy",
|
"patch:build:deploy": "npm version patch && npm run build && npm run deploy",
|
||||||
|
"pbd": "npm run patch:build:deploy",
|
||||||
"build:preview": "npm run build && astro preview",
|
"build:preview": "npm run build && astro preview",
|
||||||
"build:deploy": "npm run build && npm run deploy",
|
"build:deploy": "npm run build && npm run deploy",
|
||||||
"copy:dist": "npm run build && cp -R dist ../ayco.io-flask/",
|
"copy:dist": "npm run build && cp -R dist ../ayco.io-flask/",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"prep:now": "npx jiti ./commands/prep-now.js"
|
"prep:now": "npx jiti ./commands/prep-now.js",
|
||||||
|
"test": "vitest run ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/sitemap": "^3.7.2",
|
"@astrojs/sitemap": "^3.7.2",
|
||||||
"@ayco/astro-sw": "^0.10.1",
|
"@ayco/astro-sw": "^1.0.0",
|
||||||
"@eslint/compat": "^2.1.0",
|
"@eslint/compat": "^2.1.0",
|
||||||
|
"@eslint/config-helpers": "^0.6.0",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@iconify-json/mdi": "^1.2.3",
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
"@iconify-json/simple-icons": "^1.2.81",
|
"@iconify-json/simple-icons": "^1.2.82",
|
||||||
"@iconify-json/tabler": "^1.2.34",
|
"@iconify-json/tabler": "^1.2.35",
|
||||||
"@typescript-eslint/parser": "^8.59.2",
|
"@typescript-eslint/parser": "^8.59.3",
|
||||||
"astro": "^6.3.1",
|
"astro": "^6.3.3",
|
||||||
"astro-eslint-parser": "^1.4.0",
|
"astro-eslint-parser": "^1.4.0",
|
||||||
"astro-github-stats": "^0.8.0",
|
"astro-github-stats": "^0.8.0",
|
||||||
"astro-icon": "^1.1.5",
|
"astro-icon": "^1.1.5",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
"eslint": "^10.3.0",
|
"eslint": "^10.4.0",
|
||||||
"eslint-plugin-astro": "^1.7.0",
|
"eslint-plugin-astro": "^1.7.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jiti": "^2.7.0",
|
"jiti": "^2.7.0",
|
||||||
"lint-staged": "^17.0.2",
|
"lint-staged": "^17.0.4",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
"rehype-stringify": "^10.0.1",
|
"rehype-stringify": "^10.0.1",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^11.1.2",
|
"remark-rehype": "^11.1.2",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.59.2",
|
"typescript-eslint": "^8.59.3",
|
||||||
"unified": "^11.0.5"
|
"unified": "^11.0.5",
|
||||||
|
"vite-plugin-static-copy": "^4.1.0",
|
||||||
|
"vitest": "^4.1.7",
|
||||||
|
"vitest-dom": "^0.1.1"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,mjs,astro,ts}": [
|
"*.{js,mjs,astro,ts}": [
|
||||||
"prettier --write",
|
"prettier . --config prettier.config.mjs --write",
|
||||||
"eslint --fix"
|
"eslint . --config eslint.config.mjs --fix"
|
||||||
],
|
],
|
||||||
"*.json": [
|
"*.json": [
|
||||||
"prettier --write"
|
"prettier . --config prettier.config.mjs --write"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.33.2"
|
"packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916",
|
||||||
|
"dependencies": {
|
||||||
|
"@ayo-run/status-indicator": "^2.1.2",
|
||||||
|
"web-component-base": "^4.1.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
980
pnpm-lock.yaml
980
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
allowBuilds:
|
||||||
|
esbuild: false
|
||||||
|
sharp: false
|
||||||
|
web-component-base: false
|
||||||
13
public/publickey.asc
Normal file
13
public/publickey.asc
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mDMEahs0NhYJKwYBBAHaRw8BAQdAV2CfblniKxklPgW9eYt2gBl0jMHLwtjrQaY+
|
||||||
|
BQoWdIa0FkF5byBBeWNvIDxheW9AYXljby5pbz6ImQQTFgoAQRYhBBfxPV6P9zcr
|
||||||
|
E1RcOGXmv2QVKTxlBQJqGzQ2AhsDBQkB4TOABQsJCAcCAiICBhUKCQgLAgQWAgMB
|
||||||
|
Ah4HAheAAAoJEGXmv2QVKTxlIeYA/2WLvkDapBbVmGXoACAhqcTN93/CoPyzUbhN
|
||||||
|
hHE6mmAUAQDHsgCHmh3S/Rn4NRE3Gb41kiPTBWHrlYLTj3Kiw0ASB7g4BGobNDYS
|
||||||
|
CisGAQQBl1UBBQEBB0C6OEKEIPLaNDOM6JYhvvq0Q4Mk/B1eyZBKF/b4fQvlGgMB
|
||||||
|
CAeIfgQYFgoAJhYhBBfxPV6P9zcrE1RcOGXmv2QVKTxlBQJqGzQ2AhsMBQkB4TOA
|
||||||
|
AAoJEGXmv2QVKTxlMSQBAP5ta1kUFp3HAYwcun8qmoiVq1dEJSN1LnI7HlX4ucTl
|
||||||
|
AP0YLC768PFTBm9CM5T1BE0xjJ7s4dZSrVoI4n8RSe1nCA==
|
||||||
|
=MTsv
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
@ -54,4 +54,19 @@ const baseURL = Astro.site?.toString().slice(0, -1) // ?? 'https://ayo.ayco.io'
|
||||||
<link rel="icon" href="favicon.svg" />
|
<link rel="icon" href="favicon.svg" />
|
||||||
<link rel="mask-icon" href="mask-icon.svg" color="#000000" />
|
<link rel="mask-icon" href="mask-icon.svg" color="#000000" />
|
||||||
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
||||||
|
|
||||||
|
<!-- Web Components -->
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"web-component-base": "/wc/node_modules/web-component-base/dist/index.js",
|
||||||
|
"status-indicator": "/wc/node_modules/@ayo-run/status-indicator/dist/status-indicator.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script type="module">
|
||||||
|
// importing is enough to register the elements
|
||||||
|
// eslint-disable-next-line
|
||||||
|
import StatusIndicator from 'status-indicator'
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export const footerLinks: Link[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Mastodon',
|
text: 'Mastodon',
|
||||||
url: 'https://main.elk.zone/social.ayco.io/@ayo',
|
url: 'https://elk.zone/m.webtoo.ls/@ayo',
|
||||||
icon: 'mastodon',
|
icon: 'mastodon',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { getImage } from 'astro:assets'
|
||||||
|
|
||||||
// fetch mastodon account
|
// fetch mastodon account
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
'https://social.ayco.io/api/v1/accounts/lookup?acct=ayo'
|
'https://m.webtoo.ls/api/v1/accounts/lookup?acct=ayo'
|
||||||
)
|
)
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
const { avatar } = data
|
const { avatar } = data
|
||||||
|
|
@ -51,13 +51,28 @@ const ogFileType = 'image/png'
|
||||||
font-size: var(--font-size-base);
|
font-size: var(--font-size-base);
|
||||||
color: var(--text-color-dark);
|
color: var(--text-color-dark);
|
||||||
background-color: var(--text-color-light);
|
background-color: var(--text-color-light);
|
||||||
|
|
||||||
|
background-image: radial-gradient(
|
||||||
|
circle,
|
||||||
|
hsl(var(--dot-grid-light)) 1px,
|
||||||
|
transparent 1px
|
||||||
|
);
|
||||||
|
background-size: 24px 24px;
|
||||||
|
background-position: -24px -24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
* {
|
* {
|
||||||
background: var(--bg-darker);
|
background-color: var(--bg-darker);
|
||||||
color: var(--text-color-light);
|
color: var(--text-color-light);
|
||||||
|
|
||||||
|
background-image: radial-gradient(
|
||||||
|
circle,
|
||||||
|
hsl(var(--dot-grid-dark)) 1px,
|
||||||
|
transparent 1px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,10 +142,10 @@ const ogFileType = 'image/png'
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<!-- Anonymous RUM for web perf by Cloudflare Web Analytics -->
|
<!-- Anonymous RUM for web perf by Cloudflare Web Analytics -->
|
||||||
<script
|
<!-- <script
|
||||||
defer
|
defer
|
||||||
src="https://static.cloudflareinsights.com/beacon.min.js"
|
src="https://static.cloudflareinsights.com/beacon.min.js"
|
||||||
data-cf-beacon='{"token": "a39ad600e67a4db8960c639d2552435c"}'></script>
|
data-cf-beacon='{"token": "a39ad600e67a4db8960c639d2552435c"}'></script> -->
|
||||||
<!-- End Cloudflare Web Analytics -->
|
<!-- End Cloudflare Web Analytics -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,7 @@
|
||||||
--bg-dark: #343a40;
|
--bg-dark: #343a40;
|
||||||
--bg-darker: #212529;
|
--bg-darker: #212529;
|
||||||
--bg-darkest: #000;
|
--bg-darkest: #000;
|
||||||
|
|
||||||
|
--dot-grid-light: 214 32% 82%;
|
||||||
|
--dot-grid-dark: 215 25% 25%;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,9 @@ import Footer from '../components/Footer.astro'
|
||||||
<h2 id="contact">Contact info</h2>
|
<h2 id="contact">Contact info</h2>
|
||||||
<p>My inbox is open to everyone.</p>
|
<p>My inbox is open to everyone.</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>✉️ Email me: <a href="mailto:hi@ayo.run">hi@ayo.run</a></li>
|
|
||||||
<li>
|
<li>
|
||||||
💬 Let's chat: <a href="https://chat.ayo.run/join/7IKG-h3nW-pD1H"
|
✉️ Email me: <a href="mailto:ayo@ayco.io">ayo@ayco.io</a> ·
|
||||||
>chat.ayo.run</a
|
<small> (<a href="/pgp">PGP key</a>)</small>
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
💬 Signal: <a
|
💬 Signal: <a
|
||||||
|
|
@ -68,11 +66,11 @@ import Footer from '../components/Footer.astro'
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
</Layout>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Picture } from 'astro:assets'
|
||||||
|
|
||||||
// fetch mastodon account
|
// fetch mastodon account
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
'https://social.ayco.io/api/v1/accounts/lookup?acct=ayo'
|
'https://m.webtoo.ls/api/v1/accounts/lookup?acct=ayo'
|
||||||
)
|
)
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
const { avatar, note } = data
|
const { avatar, note } = data
|
||||||
|
|
@ -36,23 +36,22 @@ const avatarSize = 150
|
||||||
<h1 title="Ayo Ayco | Software Engineer + Web Developer">
|
<h1 title="Ayo Ayco | Software Engineer + Web Developer">
|
||||||
Hi, I'm <span class="heavy-text">Ayo</span>!
|
Hi, I'm <span class="heavy-text">Ayo</span>!
|
||||||
</h1>
|
</h1>
|
||||||
<!--a href="https://forms.ayo.run/form/tnz7FybY" class="now-wrapper"-->
|
|
||||||
<a href="/now" class="now-wrapper">
|
<a href="/now" class="now-wrapper action primary">
|
||||||
<span class="now-label">now</span>
|
<span class="now-label">
|
||||||
|
<status-indicator status="active" pulse> now </status-indicator>
|
||||||
|
</span>
|
||||||
<span class="status">{now.title}</span>
|
<span class="status">{now.title}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<main>
|
<main class=".dot-grid">
|
||||||
<section class="introduction-section">
|
<section class="introduction-section">
|
||||||
<p>
|
<p>
|
||||||
I care about the <em>Web</em>, and I love to <em>create</em> stuff to <em
|
I care about the <em>Web</em>, and I love to <em>create</em> stuff to <em
|
||||||
>inspire</em
|
>inspire</em
|
||||||
> and <em>serve</em> others.
|
> and <em>serve</em> others.
|
||||||
<!--
|
|
||||||
<a href="/about">More?</a>
|
|
||||||
-->
|
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section class="cards-section">
|
<section class="cards-section">
|
||||||
|
|
@ -86,14 +85,31 @@ const avatarSize = 150
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.now-wrapper {
|
.action {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
margin-right: 0.5rem;
|
||||||
border-radius: 40px;
|
|
||||||
padding: 8px 4px;
|
padding: 8px 4px;
|
||||||
font-weight: normal;
|
|
||||||
color: white;
|
color: white;
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-style: dotted;
|
||||||
|
border-radius: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.now-wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 390px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: clip;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.status {
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.now-label {
|
.now-label {
|
||||||
|
|
@ -193,7 +209,7 @@ const avatarSize = 150
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-device-width: 360px) {
|
@media only screen and (max-device-width: 360px) {
|
||||||
.now-wrapper {
|
.action.primary {
|
||||||
border: 0px;
|
border: 0px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
|
|
@ -224,6 +240,12 @@ const avatarSize = 150
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-device-width: 280px) and (max-device-width: 653px) {
|
@media only screen and (min-device-width: 280px) and (max-device-width: 653px) {
|
||||||
|
.action {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.now-wrapper {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
.highlighted-section__content {
|
.highlighted-section__content {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ publishedOn = publishedOn === '' ? publishDate : publishedOn
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Fragment set:html={content} />
|
<Fragment set:html={content} />
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
132
src/pages/pgp.astro
Normal file
132
src/pages/pgp.astro
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
---
|
||||||
|
import Layout from '../layouts/Layout.astro'
|
||||||
|
import Footer from '../components/Footer.astro'
|
||||||
|
|
||||||
|
const title = 'PGP public key'
|
||||||
|
const description =
|
||||||
|
'Use to verify my digital signature or to encrypt messages intended only for me.'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={"Ayo's " + title} description={description}>
|
||||||
|
<main>
|
||||||
|
<h1>My {title}</h1>
|
||||||
|
<p>{description}</p>
|
||||||
|
|
||||||
|
<div class="key-block" role="region" aria-labelledby="public-key">
|
||||||
|
<div class="btn-wrapper">
|
||||||
|
<button class="copy-btn btn" aria-label="Copy key to clipboard"
|
||||||
|
>Copy</button
|
||||||
|
>
|
||||||
|
<a href="/publickey.asc" class="btn">Download</a>
|
||||||
|
</div>
|
||||||
|
<pre
|
||||||
|
id="public-key"><code>
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mDMEahs0NhYJKwYBBAHaRw8BAQdAV2CfblniKxklPgW9eYt2gBl0jMHLwtjrQaY+
|
||||||
|
BQoWdIa0FkF5byBBeWNvIDxheW9AYXljby5pbz6ImQQTFgoAQRYhBBfxPV6P9zcr
|
||||||
|
E1RcOGXmv2QVKTxlBQJqGzQ2AhsDBQkB4TOABQsJCAcCAiICBhUKCQgLAgQWAgMB
|
||||||
|
Ah4HAheAAAoJEGXmv2QVKTxlIeYA/2WLvkDapBbVmGXoACAhqcTN93/CoPyzUbhN
|
||||||
|
hHE6mmAUAQDHsgCHmh3S/Rn4NRE3Gb41kiPTBWHrlYLTj3Kiw0ASB7g4BGobNDYS
|
||||||
|
CisGAQQBl1UBBQEBB0C6OEKEIPLaNDOM6JYhvvq0Q4Mk/B1eyZBKF/b4fQvlGgMB
|
||||||
|
CAeIfgQYFgoAJhYhBBfxPV6P9zcrE1RcOGXmv2QVKTxlBQJqGzQ2AhsMBQkB4TOA
|
||||||
|
AAoJEGXmv2QVKTxlMSQBAP5ta1kUFp3HAYwcun8qmoiVq1dEJSN1LnI7HlX4ucTl
|
||||||
|
AP0YLC768PFTBm9CM5T1BE0xjJ7s4dZSrVoI4n8RSe1nCA==
|
||||||
|
=MTsv
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
<p>Expiry Date: 2027-05-30</p>
|
||||||
|
<p>
|
||||||
|
Fingerprint: <code>17F1 3D5E 8FF7 372B 1354 5C38 65E6 BF64 1529 3C65</code
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p></p>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* Clipboard copy logic */
|
||||||
|
const copyBtn = document.querySelector('.copy-btn')
|
||||||
|
if (copyBtn) {
|
||||||
|
copyBtn.addEventListener('click', async () => {
|
||||||
|
const keyText = (
|
||||||
|
document.querySelector('#public-key code') as HTMLElement
|
||||||
|
)?.innerText.trim()
|
||||||
|
if (!keyText) return
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(keyText)
|
||||||
|
const btn = document.querySelector('.copy-btn')
|
||||||
|
if (btn) {
|
||||||
|
const original = btn.textContent
|
||||||
|
btn.textContent = 'Copied!'
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = original
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Copy failed', err)
|
||||||
|
alert('Unable to copy the key. Please copy it manually.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
code {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-block {
|
||||||
|
position: relative;
|
||||||
|
background: #272822;
|
||||||
|
color: #f8f8f2;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding: 1rem 1rem 1rem 1.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-block code {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* buttons */
|
||||||
|
.btn-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
background: var(--color-brand-blue-1);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover,
|
||||||
|
.btn:focus {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus {
|
||||||
|
outline: 2px solid #0056b3;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -11,7 +11,7 @@ import Card from '../components/Card.astro'
|
||||||
<main>
|
<main>
|
||||||
<h1><span class="text-gradient">Hobby Projects</span></h1>
|
<h1><span class="text-gradient">Hobby Projects</span></h1>
|
||||||
<p>
|
<p>
|
||||||
See more of my previous projects at <a href="https://ayos.blog"
|
See more of my previous projects at <a href="https://ayos.blog/projects"
|
||||||
>my blog</a
|
>my blog</a
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
121
tests/prep-now.test.ts
Normal file
121
tests/prep-now.test.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { readFileSync, writeFileSync, copyFileSync } from 'node:fs'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { dirname, resolve } from 'node:path'
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { consola } from 'consola'
|
||||||
|
|
||||||
|
import newNow from '../commands/prep-now'
|
||||||
|
|
||||||
|
// Mock file system operations
|
||||||
|
vi.mock('path', () => ({
|
||||||
|
resolve: vi.fn(),
|
||||||
|
readFileSync: vi.fn(),
|
||||||
|
writeFileSync: vi.fn(),
|
||||||
|
copyFileSync: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock consola
|
||||||
|
vi.mock('consola', () => ({
|
||||||
|
consola: {
|
||||||
|
box: vi.fn(),
|
||||||
|
start: vi.fn(),
|
||||||
|
success: vi.fn(),
|
||||||
|
fail: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock mdToHTML
|
||||||
|
vi.mock('../command/md-to-html', () => ({
|
||||||
|
mdToHTML: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('prep-now', () => {
|
||||||
|
// const mockNow = {
|
||||||
|
// title: 'Test Title',
|
||||||
|
// description: 'Test Description',
|
||||||
|
// publishedOn: '2023-01-01',
|
||||||
|
// publishDate: '2023-01-01',
|
||||||
|
// publishState: 'draft',
|
||||||
|
// content: '# Test Content',
|
||||||
|
// }
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
vi.mocked(resolve).mockImplementation((...paths) => paths.join('/'))
|
||||||
|
vi.mocked(readFileSync).mockReturnValue('template content')
|
||||||
|
vi.mocked(writeFileSync).mockImplementation(() => {})
|
||||||
|
vi.mocked(copyFileSync).mockImplementation(() => {})
|
||||||
|
vi.mocked(consola.box).mockImplementation(() => {})
|
||||||
|
vi.mocked(consola.start).mockImplementation(() => {})
|
||||||
|
vi.mocked(consola.success).mockImplementation(() => {})
|
||||||
|
vi.mocked(consola.fail).mockImplementation(() => {})
|
||||||
|
vi.mocked(consola.error).mockImplementation(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create a new now page and clear the original files', async () => {
|
||||||
|
// Mock the mdToHTML function
|
||||||
|
const { mdToHTML } = await import('../utils/md-to-html')
|
||||||
|
vi.mocked(mdToHTML).mockResolvedValue('<h1>Test Content</h1>')
|
||||||
|
|
||||||
|
// Mock file paths
|
||||||
|
vi.mocked(resolve)
|
||||||
|
.mockReturnValueOnce('/src/constants/now.md')
|
||||||
|
.mockReturnValueOnce('/src/constants/now.json')
|
||||||
|
.mockReturnValueOnce('/src/pages/now/and-then/posts/2023-01-01.astro')
|
||||||
|
.mockReturnValueOnce('/src/constants/bkup/2023-01-01.md')
|
||||||
|
.mockReturnValueOnce('/src/constants/bkup/2023-01-01.json')
|
||||||
|
|
||||||
|
// Mock file content
|
||||||
|
vi.mocked(readFileSync)
|
||||||
|
.mockReturnValueOnce('template content')
|
||||||
|
.mockReturnValueOnce('# Test Content')
|
||||||
|
|
||||||
|
// Mock file paths
|
||||||
|
const __filename = '/src/commands/prep-now.ts'
|
||||||
|
const __dirname = '/src/commands'
|
||||||
|
vi.mocked(fileURLToPath).mockReturnValue(__filename)
|
||||||
|
vi.mocked(dirname).mockReturnValue(__dirname)
|
||||||
|
|
||||||
|
// Execute the function
|
||||||
|
await newNow()
|
||||||
|
|
||||||
|
// Verify file operations
|
||||||
|
expect(writeFileSync).toHaveBeenCalledTimes(2)
|
||||||
|
expect(copyFileSync).toHaveBeenCalledTimes(2)
|
||||||
|
expect(consola.success).toHaveBeenCalledWith('now.md cleared')
|
||||||
|
expect(consola.success).toHaveBeenCalledWith(
|
||||||
|
'You may now update your Now content and props.\n'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
// Mock an error
|
||||||
|
vi.mocked(writeFileSync).mockImplementation(() => {
|
||||||
|
throw new Error('Write failed')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock file paths
|
||||||
|
vi.mocked(resolve)
|
||||||
|
.mockReturnValueOnce('/src/constants/now.md')
|
||||||
|
.mockReturnValueOnce('/src/constants/now.json')
|
||||||
|
.mockReturnValueOnce('/src/pages/now/and-then/posts/2023-01-01.astro')
|
||||||
|
.mockReturnValueOnce('/src/constants/bkup/2023-01-01.md')
|
||||||
|
.mockReturnValueOnce('/src/constants/bkup/2023-01-01.json')
|
||||||
|
|
||||||
|
// Mock file content
|
||||||
|
vi.mocked(readFileSync)
|
||||||
|
.mockReturnValueOnce('template content')
|
||||||
|
.mockReturnValueOnce('# Test Content')
|
||||||
|
|
||||||
|
// Execute the function
|
||||||
|
await newNow()
|
||||||
|
|
||||||
|
// Verify error handling
|
||||||
|
expect(consola.fail).toHaveBeenCalledWith(
|
||||||
|
'Failed to create a new post from Now'
|
||||||
|
)
|
||||||
|
expect(consola.error).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
91
tests/sw.test.js
Normal file
91
tests/sw.test.js
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { install } from 'vitest-dom'
|
||||||
|
|
||||||
|
// Install DOM environment for vitest
|
||||||
|
install()
|
||||||
|
|
||||||
|
// Mock the service worker globals
|
||||||
|
global.caches = {
|
||||||
|
keys: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
open: vi.fn(),
|
||||||
|
match: vi.fn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock the global self object
|
||||||
|
global.self = {
|
||||||
|
skipWaiting: vi.fn(),
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock the fetch function
|
||||||
|
global.fetch = vi.fn()
|
||||||
|
|
||||||
|
// Import the service worker module
|
||||||
|
// Note: We need to use dynamic import to avoid module loading issues
|
||||||
|
let swModule
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Clear all mocks
|
||||||
|
vi.clearAllMocks()
|
||||||
|
|
||||||
|
// Mock the module
|
||||||
|
swModule = await import('../src/sw.mjs')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Service Worker', () => {
|
||||||
|
describe('cleanOldCaches', () => {
|
||||||
|
it('should delete old caches', async () => {
|
||||||
|
const cacheName = 'app-v000'
|
||||||
|
const oldCacheName = 'app-v001'
|
||||||
|
|
||||||
|
global.caches.keys.mockResolvedValue([cacheName, oldCacheName])
|
||||||
|
global.caches.delete.mockResolvedValue(true)
|
||||||
|
|
||||||
|
await swModule.cleanOldCaches()
|
||||||
|
|
||||||
|
expect(global.caches.delete).toHaveBeenCalledWith(oldCacheName)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('addResourcesToCache', () => {
|
||||||
|
it('should add resources to cache', async () => {
|
||||||
|
const resources = ['/index.html', '/style.css']
|
||||||
|
const cache = { addAll: vi.fn() }
|
||||||
|
|
||||||
|
global.caches.open.mockResolvedValue(cache)
|
||||||
|
|
||||||
|
await swModule.addResourcesToCache(resources)
|
||||||
|
|
||||||
|
expect(cache.addAll).toHaveBeenCalledWith(resources)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('networkFirst', () => {
|
||||||
|
it('should return network response when available', async () => {
|
||||||
|
const request = new Request('/test')
|
||||||
|
const networkResponse = new Response('network content')
|
||||||
|
|
||||||
|
global.fetch.mockResolvedValue(networkResponse)
|
||||||
|
global.caches.open.mockResolvedValue({ put: vi.fn() })
|
||||||
|
|
||||||
|
const result = await swModule.networkFirst({ request })
|
||||||
|
|
||||||
|
expect(result).toEqual(networkResponse)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return cached response when network fails', async () => {
|
||||||
|
const request = new Request('/test')
|
||||||
|
const cachedResponse = new Response('cached content')
|
||||||
|
|
||||||
|
global.fetch.mockRejectedValue(new Error('Network error'))
|
||||||
|
global.caches.open.mockResolvedValue({
|
||||||
|
match: vi.fn().mockResolvedValue(cachedResponse),
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await swModule.networkFirst({ request })
|
||||||
|
|
||||||
|
expect(result).toEqual(cachedResponse)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
14
vitest.config.ts
Normal file
14
vitest.config.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
// Exclude generated files and heavy folders from the watcher to avoid
|
||||||
|
// continuous re-runs when build output or other tools touch files.
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'node',
|
||||||
|
exclude: ['dist/**', 'public/**', 'node_modules/**'],
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
exclude: ['dist/**', 'public/**', 'node_modules/**', '.git/**'],
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue