1
0
mirror of https://github.com/kremalicious/blog.git synced 2025-02-14 21:10:25 +01:00

migrate to biome (#944)

* migrate to biome

* cleanup

* fix

* use tsx

* script tweaks

* fix test runs

* path tweaks
This commit is contained in:
Matthias Kretschmann 2024-07-27 21:15:05 +01:00 committed by GitHub
parent d11617c2b2
commit 05ad0470c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
148 changed files with 3332 additions and 5953 deletions

View File

@ -1,29 +0,0 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-css-modules",
"stylelint-prettier/recommended"
],
"plugins": ["stylelint-prettier"],
"syntax": "css",
"rules": {
"prettier/prettier": true,
"property-no-unknown": [
true,
{
"ignoreProperties": ["composes", "compose-with"]
}
],
"at-rule-no-unknown": [
true,
{
"ignoreAtRules": ["value", "include", "mixin", "extend"]
}
],
"selector-class-pattern": null,
"keyframes-name-pattern": null,
"custom-property-pattern": null,
"selector-id-pattern": null,
"media-feature-range-notation": "prefix"
}
}

View File

@ -1,12 +1,13 @@
import { defineConfig } from 'astro/config'
import { remarkLeadParagraph } from '../src/lib/remark-lead-paragraph/remark-lead-paragraph'
import { remarkToc } from '../src/lib/remark-toc/remark-toc'
import react from '@astrojs/react' import react from '@astrojs/react'
import sitemap from '@astrojs/sitemap' import sitemap from '@astrojs/sitemap'
import type { RemarkPlugins } from 'astro'
import expressiveCode from 'astro-expressive-code' import expressiveCode from 'astro-expressive-code'
import redirectFrom from 'astro-redirect-from' import redirectFrom from 'astro-redirect-from'
import config from './blog.config' import { defineConfig } from 'astro/config'
import { getSlug } from '../src/lib/astro/getSlug' import { getSlug } from '../src/lib/astro/getSlug'
import { remarkLeadParagraph } from '../src/lib/remark-lead-paragraph/remark-lead-paragraph'
import { remarkToc } from '../src/lib/remark-toc/remark-toc'
import config from './blog.config'
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
@ -15,7 +16,7 @@ export default defineConfig({
cacheDir: '.astro', cacheDir: '.astro',
trailingSlash: 'always', trailingSlash: 'always',
markdown: { markdown: {
remarkPlugins: [remarkLeadParagraph, remarkToc as any], remarkPlugins: [remarkLeadParagraph, remarkToc] as unknown as RemarkPlugins,
shikiConfig: { shikiConfig: {
// https://github.com/shikijs/shiki/blob/main/docs/themes.md // https://github.com/shikijs/shiki/blob/main/docs/themes.md
theme: 'nord', theme: 'nord',

View File

@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged npx lint-staged

View File

@ -1,14 +0,0 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"tabWidth": 2,
"endOfLine": "lf",
"plugins": ["prettier-plugin-astro"],
"overrides": [
{
"files": "*.astro",
"options": { "parser": "astro" }
}
]
}

View File

@ -140,7 +140,7 @@ If you want to know how, have a look at the respective components:
All SVG assets under `src/images/` and from select iconset dependencies are converted to Astro & React components before building the site. Compiled components are placed under `src/images/components/` and all include the cleaned SVGs as inline HTML. All SVG assets under `src/images/` and from select iconset dependencies are converted to Astro & React components before building the site. Compiled components are placed under `src/images/components/` and all include the cleaned SVGs as inline HTML.
All SVGs can then be imported from `@images/components` in all Astro or React components. All SVGs can then be imported from `@/images/components` in all Astro or React components.
If you want to know how this works, have a look at the script: If you want to know how this works, have a look at the script:
@ -177,18 +177,12 @@ npm start
### 🔮 Linting ### 🔮 Linting
ESlint, Prettier, and Stylelint are setup for all linting purposes: [Biome](https://biomejs.dev) is setup for all linting and formatting purposes:
```bash ```bash
npm run lint npm run lint
``` ```
To automatically format all code files:
```bash
npm run format
```
### 🔮 Type Checking ### 🔮 Type Checking
Type checking can be invoked to check all TypeScript code, including within .astro files: Type checking can be invoked to check all TypeScript code, including within .astro files:

26
biome.json Normal file
View File

@ -0,0 +1,26 @@
{
"extends": ["@kremalicious/config/biome"],
"overrides": [
{
"include": ["*.astro"],
"linter": {
"rules": {
"style": {
"useConst": "off",
"useImportType": "off"
}
}
}
},
{
"include": ["*.test.ts", "*.test.tsx"],
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
}
}
}
}
]
}

View File

@ -1,4 +1,4 @@
import { z, type ImageFunction } from 'astro:content' import { type ImageFunction, z } from 'astro:content'
const schemaShared = { const schemaShared = {
title: z.string(), title: z.string(),

View File

@ -21,11 +21,7 @@ article kbd {
rgb(0 0 0 / 0%) rgb(0 0 0 / 0%)
); );
background-repeat: repeat-x; background-repeat: repeat-x;
box-shadow: box-shadow: 0 2px 0 #bbb, 0 3px 1px #999, 0 3px 0 #bbb, inset 0 1px 1px #fff,
0 2px 0 #bbb,
0 3px 1px #999,
0 3px 0 #bbb,
inset 0 1px 1px #fff,
inset 0 -1px 3px #ccc; inset 0 -1px 3px #ccc;
} }
@ -40,11 +36,8 @@ article kbd.dark {
rgb(0 0 0 / 0%) rgb(0 0 0 / 0%)
); );
background-repeat: no-repeat; background-repeat: no-repeat;
box-shadow: box-shadow: 0 2px 0 #000, 0 3px 1px #999, inset 0 1px 1px #aaa, inset 0 -1px
0 2px 0 #000, 3px #272727;
0 3px 1px #999,
inset 0 1px 1px #aaa,
inset 0 -1px 3px #272727;
} }
article kbd.ios { article kbd.ios {
@ -55,10 +48,8 @@ article kbd.ios {
background-color: #b7b7bc; background-color: #b7b7bc;
background-image: linear-gradient(to bottom, #efeff0, #b7b7bc); background-image: linear-gradient(to bottom, #efeff0, #b7b7bc);
background-repeat: repeat-x; background-repeat: repeat-x;
box-shadow: box-shadow: 0 1px 2px rgb(0 0 0 / 60%), 0 2px 3px rgb(0 0 0 / 10%), inset 0
0 1px 2px rgb(0 0 0 / 60%), 1px 0 #fff;
0 2px 3px rgb(0 0 0 / 10%),
inset 0 1px 0 #fff;
} }
article kbd.android { article kbd.android {
@ -70,24 +61,15 @@ article kbd.android {
border-radius: 3px; border-radius: 3px;
background-clip: padding-box; background-clip: padding-box;
background-color: #5e5e5e; background-color: #5e5e5e;
box-shadow: box-shadow: 0 2px 2px rgb(0 0 0 / 30%), 0 1px 0 #444, inset 0 1px 0 #868686;
0 2px 2px rgb(0 0 0 / 30%),
0 1px 0 #444,
inset 0 1px 0 #868686;
} }
article kbd.android.dark { article kbd.android.dark {
background: #222; background: #222;
box-shadow: box-shadow: 0 2px 2px rgb(0 0 0 / 70%), 0 1px 0 #444, inset 0 1px 0 #505050;
0 2px 2px rgb(0 0 0 / 70%),
0 1px 0 #444,
inset 0 1px 0 #505050;
} }
article kbd.android.color { article kbd.android.color {
background: #083c5b; background: #083c5b;
box-shadow: box-shadow: 0 2px 2px rgb(0 0 0 / 70%), 0 1px 0 #444, inset 0 1px 0 #36647b;
0 2px 2px rgb(0 0 0 / 70%),
0 1px 0 #444,
inset 0 1px 0 #36647b;
} }

View File

@ -1,39 +0,0 @@
import eslint from '@eslint/js'
import tseslint from 'typescript-eslint'
import prettierRecommended from 'eslint-plugin-prettier/recommended'
import eslintPluginAstro from 'eslint-plugin-astro'
import globals from 'globals'
import gitignore from 'eslint-config-flat-gitignore'
import testingLibrary from 'eslint-plugin-testing-library'
import { fixupPluginRules } from '@eslint/compat'
export default tseslint.config(
{
languageOptions: {
globals: { ...globals.browser }
}
},
gitignore(),
eslint.configs.recommended,
...tseslint.configs.recommended,
prettierRecommended,
...eslintPluginAstro.configs.recommended,
...eslintPluginAstro.configs['jsx-a11y-recommended'],
{
rules: {
'@typescript-eslint/no-explicit-any': 'warn'
// "astro/no-set-html-directive": "error"
}
},
// See https://github.com/testing-library/eslint-plugin-testing-library/issues/853
// current solution from https://github.com/testing-library/eslint-plugin-testing-library/issues/899#issuecomment-2121272355
{
files: ['**/?(*.)+(spec|test).[jt]sx'],
plugins: {
'testing-library': fixupPluginRules({
rules: testingLibrary.rules
})
},
rules: testingLibrary.configs.react.rules
}
)

8166
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,52 +17,36 @@
"test:unit": "vitest run --config './test/vitest.config.ts' --coverage", "test:unit": "vitest run --config './test/vitest.config.ts' --coverage",
"test:unit:watch": "vitest watch --config './test/vitest.config.ts' --coverage", "test:unit:watch": "vitest watch --config './test/vitest.config.ts' --coverage",
"test:e2e": "playwright test --config './test/playwright.config.ts'", "test:e2e": "playwright test --config './test/playwright.config.ts'",
"lint": "run-p --silent lint:js lint:css lint:md", "lint:md": "markdownlint --config '.config/markdownlint.json' --ignore-path .gitignore --dot './**/*.{md,markdown}'",
"lint:js": "eslint './{src,test,scripts}/**/*.{ts,tsx,astro,mjs,js,cjs}'", "lint:biome": "biome check --write .",
"lint:css": "stylelint --config '.config/.stylelintrc.json' 'src/**/*.css'", "lint": "run-p --silent lint:biome lint:md",
"lint:md": "markdownlint --config '.config/.markdownlint.json' --ignore-path .gitignore --dot './**/*.{md,markdown}'",
"format": "prettier --ignore-path .gitignore --write '**/*.{js,jsx,ts,tsx,md,json,css,astro,yml}'",
"deploy:s3": "./scripts/deploy-s3.sh", "deploy:s3": "./scripts/deploy-s3.sh",
"new": "node --loader ts-node/esm scripts/new/index.ts", "new": "tsx scripts/new/index.ts",
"create:icons": "node --loader ts-node/esm scripts/create-icons/index.ts", "create:icons": "tsx scripts/create-icons/index.ts",
"create:redirects": "node --loader ts-node/esm scripts/redirect-from.ts", "create:redirects": "tsx scripts/redirect-from.ts",
"create:symlinks": "./scripts/create-symlinks.sh", "create:symlinks": "./scripts/create-symlinks.sh",
"move:downloads": "node --loader ts-node/esm scripts/move-downloads.ts", "move:downloads": "tsx scripts/move-downloads.ts",
"prepare": "husky .config/husky", "prepare": "husky .config/husky"
"lint:staged": "lint-staged"
}, },
"lint-staged": { "lint-staged": {
"*.{js,jsx,ts,tsx,astro}": [ "*.{js,jsx,ts,tsx,astro,css,json,md}": ["biome check --write ."],
"prettier --write", "*.md": ["markdownlint --config '.config/markdownlint.json'"]
"eslint"
],
"*.css": [
"stylelint --config '.config/.stylelintrc.json' --fix",
"prettier --write"
],
"**/*.json": [
"prettier --write"
],
"*.md": [
"markdownlint --config '.config/.markdownlint.json'",
"prettier --write"
]
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.7.0", "@astrojs/check": "^0.8.3",
"@astrojs/react": "^3.6.0", "@astrojs/react": "^3.6.0",
"@astrojs/rss": "^4.0.6", "@astrojs/rss": "^4.0.7",
"@astrojs/sitemap": "^3.1.6", "@astrojs/sitemap": "^3.1.6",
"@coingecko/cryptoformat": "^0.8.1", "@coingecko/cryptoformat": "^0.8.1",
"@nanostores/persistent": "^0.10.1", "@nanostores/persistent": "^0.10.1",
"@nanostores/query": "^0.3.3", "@nanostores/query": "^0.3.3",
"@nanostores/react": "^0.7.2", "@nanostores/react": "^0.7.2",
"@radix-ui/react-select": "^2.1.1", "@radix-ui/react-select": "^2.1.1",
"@rainbow-me/rainbowkit": "^2.1.2", "@rainbow-me/rainbowkit": "^2.1.3",
"@tanstack/react-query": "^5.45.1", "@tanstack/react-query": "^5.45.1",
"astro": "4.11.0", "astro": "4.12.2",
"astro-expressive-code": "^0.35.3", "astro-expressive-code": "^0.35.3",
"astro-redirect-from": "^1.0.8", "astro-redirect-from": "^1.1.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dms2dec": "^1.1.0", "dms2dec": "^1.1.0",
"fast-exif": "^2.0.1", "fast-exif": "^2.0.1",
@ -77,32 +61,24 @@
"sharp": "^0.33.4", "sharp": "^0.33.4",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"swr": "^2.2.5", "swr": "^2.2.5",
"viem": "^2.16.1", "viem": "^2.18.2",
"wagmi": "^2.10.4" "wagmi": "^2.12.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.1.0", "@biomejs/biome": "^1.8.3",
"@eslint/js": "^9.5.0", "@kremalicious/config": "^1.0.2",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.45.3",
"@testing-library/jest-dom": "^6.4.6", "@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0", "@testing-library/react": "^16.0.0",
"@types/node": "^20.14.8", "@types/node": "^20.14.8",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@vitest/coverage-v8": "^1.6.0", "@vitest/coverage-v8": "^2.0.4",
"eslint": "^8.57.0", "@vitest/ui": "^2.0.4",
"eslint-config-flat-gitignore": "^0.1.5", "globby": "^14.0.2",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-astro": "^1.2.2",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.0",
"eslint-plugin-testing-library": "^6.2.2",
"globals": "^15.6.0",
"hast-util-to-html": "^9.0.1", "hast-util-to-html": "^9.0.1",
"husky": "^9.0.11", "husky": "^9.1.3",
"identity-obj-proxy": "^3.0.0", "jsdom": "^24.1.1",
"jsdom": "^24.1.0",
"markdownlint-cli": "^0.41.0", "markdownlint-cli": "^0.41.0",
"mdast-util-to-hast": "^13.2.0", "mdast-util-to-hast": "^13.2.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
@ -110,20 +86,13 @@
"node-iptc": "^1.0.5", "node-iptc": "^1.0.5",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"ora": "^8.0.1", "ora": "^8.0.1",
"prettier": "^3.3.2",
"prettier-plugin-astro": "^0.14.0",
"sharp-ico": "^0.1.5", "sharp-ico": "^0.1.5",
"stylelint": "^16.6.1", "svgo": "^3.3.2",
"stylelint-config-css-modules": "^4.4.0", "tsx": "^4.16.2",
"stylelint-config-standard": "^36.0.1", "typescript": "^5.5.4",
"stylelint-prettier": "^5.0.0",
"svgo": "^3.2.0",
"ts-node": "^10.9.2",
"typescript": "^5.5.2",
"typescript-eslint": "^7.13.1",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0" "vitest": "^2.0.4"
}, },
"engines": { "engines": {
"node": "20" "node": "20"
@ -133,10 +102,7 @@
"url": "https://github.com/kremalicious/blog.git" "url": "https://github.com/kremalicious/blog.git"
}, },
"browserslist": { "browserslist": {
"production": [ "production": ["defaults", ">0.2%"],
"defaults",
">0.2%"
],
"development": [ "development": [
"last 1 chrome version", "last 1 chrome version",
"last 1 firefox version", "last 1 firefox version",

View File

@ -1,14 +1,14 @@
self.addEventListener('install', function () { self.addEventListener('install', () => {
self.skipWaiting() self.skipWaiting()
}) })
self.addEventListener('activate', function () { self.addEventListener('activate', () => {
self.registration self.registration
.unregister() .unregister()
.then(function () { .then(() => self.clients.matchAll())
return self.clients.matchAll() .then((clients) => {
}) for (const client of clients) {
.then(function (clients) { client.navigate(client.url)
clients.forEach((client) => client.navigate(client.url)) }
}) })
}) })

View File

@ -1,6 +1,6 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { test } from 'vitest' import { test } from 'vitest'
import fs from 'fs/promises'
import path from 'path'
import { generateIcons } from './index' import { generateIcons } from './index'
const distDir = path.resolve(__dirname, 'tmp') const distDir = path.resolve(__dirname, 'tmp')

View File

@ -4,8 +4,8 @@
// //
import fs from 'node:fs/promises' import fs from 'node:fs/promises'
import ps from 'node:path/posix' import ps from 'node:path/posix'
import ora from 'ora'
import chalk from 'chalk' import chalk from 'chalk'
import ora from 'ora'
import { toInnerSvg } from './svg.ts' import { toInnerSvg } from './svg.ts'
import { toAstroComponent } from './toAstroComponent.ts' import { toAstroComponent } from './toAstroComponent.ts'
import { toReactComponent } from './toReactComponent.ts' import { toReactComponent } from './toReactComponent.ts'
@ -25,24 +25,26 @@ const distDir = ps.resolve(currentDir, 'src/images/components')
// Data related to each icon exported by this package. // Data related to each icon exported by this package.
const icons = [] const icons = []
export async function generateIcons(distDir: string) { export async function generateIcons(_distDir: string) {
const spinner = ora( const spinner = ora(
`${chalk.bold('[create-icons]')} Create icon components` `${chalk.bold('[create-icons]')} Create icon components`
).start() ).start()
const dist = _distDir || distDir
// clean the distribution directory // clean the distribution directory
await fs.rm(distDir, { force: true, recursive: true }) await fs.rm(dist, { force: true, recursive: true })
await fs.mkdir(distDir, { recursive: true }) await fs.mkdir(dist, { recursive: true })
await fs.mkdir(`${distDir}/react`, { recursive: true }) await fs.mkdir(`${dist}/react`, { recursive: true })
// copy the attribute typings file // copy the attribute typings file
await fs.copyFile( await fs.copyFile(
ps.resolve(currentDir, 'scripts/create-icons/Props.d.ts'), ps.resolve(currentDir, 'scripts/create-icons/Props.d.ts'),
ps.resolve(distDir, 'Props.d.ts') ps.resolve(dist, 'Props.d.ts')
) )
await fs.copyFile( await fs.copyFile(
ps.resolve(currentDir, 'scripts/create-icons/Props.d.ts'), ps.resolve(currentDir, 'scripts/create-icons/Props.d.ts'),
ps.resolve(`${distDir}/react`, 'Props.d.ts') ps.resolve(`${dist}/react`, 'Props.d.ts')
) )
// convert the SVG files into Astro & React components // convert the SVG files into Astro & React components
@ -86,14 +88,14 @@ export async function generateIcons(distDir: string) {
// write the astro component to a file // write the astro component to a file
await fs.writeFile( await fs.writeFile(
ps.resolve(distDir, `${baseName}.astro`), ps.resolve(dist, `${baseName}.astro`),
toAstroComponent(innerSVG, title), toAstroComponent(innerSVG, title),
'utf8' 'utf8'
) )
// write the react component to a file // write the react component to a file
await fs.writeFile( await fs.writeFile(
ps.resolve(`${distDir}/react`, `${baseName}.tsx`), ps.resolve(`${dist}/react`, `${baseName}.tsx`),
toReactComponent(innerSVG, title), toReactComponent(innerSVG, title),
'utf8' 'utf8'
) )
@ -109,11 +111,11 @@ export async function generateIcons(distDir: string) {
} }
// write the main Astro entry `index.ts` file // write the main Astro entry `index.ts` file
await fs.writeFile(ps.resolve(distDir, 'index.ts'), contentOfIndexJS, 'utf8') await fs.writeFile(ps.resolve(dist, 'index.ts'), contentOfIndexJS, 'utf8')
// write the main React entry `react/index.ts` file // write the main React entry `react/index.ts` file
await fs.writeFile( await fs.writeFile(
ps.resolve(`${distDir}/react`, 'index.ts'), ps.resolve(`${dist}/react`, 'index.ts'),
contentOfIndexReactJS, contentOfIndexReactJS,
'utf8' 'utf8'
) )
@ -121,7 +123,7 @@ export async function generateIcons(distDir: string) {
spinner.succeed( spinner.succeed(
`${chalk.bold('[create-icons]')} Generated ${ `${chalk.bold('[create-icons]')} Generated ${
icons.length icons.length
} icons into @images/components.` } icons into @/images/components.`
) )
} }

View File

@ -1,10 +1,11 @@
import { test, expect, vi } from 'vitest'
import fs from 'node:fs/promises' import fs from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
import { glob } from 'glob'
import { copyZipFiles } from './move-downloads'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import chalk from 'chalk' import chalk from 'chalk'
import { glob } from 'glob'
import type { Ora } from 'ora'
import { expect, test, vi } from 'vitest'
import { copyZipFiles } from './move-downloads'
const __dirname = path.dirname(fileURLToPath(import.meta.url)) const __dirname = path.dirname(fileURLToPath(import.meta.url))
@ -21,12 +22,13 @@ test('copyZipFiles should copy zip files', async () => {
globMock.mockReturnValue(['file1.zip', 'file2.zip']) globMock.mockReturnValue(['file1.zip', 'file2.zip'])
const mockOra = { const mockOra = {
text: '',
start: vi.fn(), start: vi.fn(),
succeed: vi.fn(), succeed: vi.fn(),
fail: vi.fn() fail: vi.fn()
} }
await copyZipFiles(sourceDir, destDir, mockOra as any) await copyZipFiles(sourceDir, destDir, mockOra as unknown as Ora)
const file1 = await fs.readFile(path.join(destDir, 'file1.zip'), 'utf-8') const file1 = await fs.readFile(path.join(destDir, 'file1.zip'), 'utf-8')
const file2 = await fs.readFile(path.join(destDir, 'file2.zip'), 'utf-8') const file2 = await fs.readFile(path.join(destDir, 'file2.zip'), 'utf-8')

View File

@ -4,9 +4,9 @@
// //
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import globby from 'globby'
import ora, { type Ora } from 'ora'
import chalk from 'chalk' import chalk from 'chalk'
import { globby } from 'globby'
import ora, { type Ora } from 'ora'
const sourceFolder = './content/articles/' const sourceFolder = './content/articles/'
const destinationFolder = './public/get/' const destinationFolder = './public/get/'
@ -18,7 +18,7 @@ const spinner = ora(
function removeFolderContents(folderPath: string) { function removeFolderContents(folderPath: string) {
if (fs.existsSync(folderPath)) { if (fs.existsSync(folderPath)) {
fs.readdirSync(folderPath).forEach((file) => { for (const file of fs.readdirSync(folderPath)) {
const filePath = path.join(folderPath, file) const filePath = path.join(folderPath, file)
if (fs.lstatSync(filePath).isDirectory()) { if (fs.lstatSync(filePath).isDirectory()) {
removeFolderContents(filePath) removeFolderContents(filePath)
@ -26,7 +26,7 @@ function removeFolderContents(folderPath: string) {
} else { } else {
fs.unlinkSync(filePath) fs.unlinkSync(filePath)
} }
}) }
} }
} }
@ -46,22 +46,21 @@ export async function copyZipFiles(
// Find all files recursively in the source folder // Find all files recursively in the source folder
const zipFiles = await globby(filesGlob, { cwd: source }) const zipFiles = await globby(filesGlob, { cwd: source })
zipFiles.forEach((zipFile: string) => { for (const zipFile of zipFiles) {
const sourcePath = path.join(source, zipFile) const sourcePath = path.join(source, zipFile)
const destinationPath = path.join(destination, path.basename(zipFile)) const destinationPath = path.join(destination, path.basename(zipFile))
try { try {
// Copy the file to the destination folder // Copy the file to the destination folder
fs.copyFileSync(sourcePath, destinationPath) fs.copyFileSync(sourcePath, destinationPath)
} catch (error: any) { } catch (error: unknown) {
spinner.fail( spinner.fail(
`${chalk.bold('[move-downloads]')} Error copying ${zipFile}: ${ `${chalk.bold('[move-downloads]')} Error copying ${zipFile}: ${
(error as Error).message (error as Error).message
}` }`
) )
return
} }
}) }
spinner.succeed( spinner.succeed(
`${chalk.bold('[move-downloads]')} Copied ${ `${chalk.bold('[move-downloads]')} Copied ${

View File

@ -1,9 +1,9 @@
import fs from 'node:fs/promises'
import { existsSync, mkdirSync, readFileSync } from 'node:fs' import { existsSync, mkdirSync, readFileSync } from 'node:fs'
import fs from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { slugify } from '../../src/lib/slugify/slugify.js'
import type { Ora } from 'ora' import type { Ora } from 'ora'
import { slugify } from '../../src/lib/slugify/slugify.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url)) const __dirname = path.dirname(fileURLToPath(import.meta.url))
const templatePath = path.join(__dirname, 'new-article.md') const templatePath = path.join(__dirname, 'new-article.md')
@ -14,7 +14,7 @@ export async function createArticlePost(
title: string, title: string,
newDate?: string newDate?: string
) { ) {
let file let file = ''
const date = newDate const date = newDate
? new Date(newDate).toISOString() ? new Date(newDate).toISOString()
: new Date().toISOString() : new Date().toISOString()
@ -44,7 +44,7 @@ export async function createArticlePost(
// create post file // create post file
await fs.appendFile(file, newContents) await fs.appendFile(file, newContents)
spinner.succeed(`New post '${title}' created.`) spinner.succeed(`New post '${title}' created.`)
} catch (error: any) { } catch (error: unknown) {
spinner.fail((error as Error).message) spinner.fail((error as Error).message)
} }

View File

@ -1,9 +1,9 @@
import { existsSync, mkdirSync, readFileSync, promises as fs } from 'node:fs' import { promises as fs, existsSync, mkdirSync, readFileSync } from 'node:fs'
import { slugify } from '../../src/lib/slugify/slugify.js'
import { readOutExif } from '../../src/lib/exif/readOutExif.js'
import path from 'node:path' import path from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import type { Ora } from 'ora' import type { Ora } from 'ora'
import { readOutExif } from '../../src/lib/exif/readOutExif.js'
import { slugify } from '../../src/lib/slugify/slugify.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url)) const __dirname = path.dirname(fileURLToPath(import.meta.url))
const templatePathPhoto = path.join(__dirname, 'new-photo.md') const templatePathPhoto = path.join(__dirname, 'new-photo.md')
@ -14,10 +14,10 @@ export async function createPhotoPost(
photo: string, photo: string,
photoTitle?: string photoTitle?: string
) { ) {
let title let title: string | undefined
let titleSlug let titleSlug: string
let date let date: string
let postPhotoFile let postPhotoFile = ''
try { try {
const templatePhoto = readFileSync(templatePathPhoto).toString() const templatePhoto = readFileSync(templatePathPhoto).toString()
@ -25,7 +25,7 @@ export async function createPhotoPost(
if (!exifData) throw new Error('No exif data found in image') if (!exifData) throw new Error('No exif data found in image')
const { iptc, exif } = exifData const { iptc, exif } = exifData
title = iptc?.object_name || iptc?.title || photoTitle title = iptc?.object_name || iptc?.caption || photoTitle
if (!title) if (!title)
throw new Error( throw new Error(
'No title given. Add to IPTC, or use the format `npm run new photo path/to/photo.jpg "Title of post"' 'No title given. Add to IPTC, or use the format `npm run new photo path/to/photo.jpg "Title of post"'
@ -36,7 +36,7 @@ export async function createPhotoPost(
date = new Date(exif?.date).toISOString() date = new Date(exif?.date).toISOString()
const dateShort = date.slice(0, 10) const dateShort = date.slice(0, 10)
const description = iptc?.caption const description = iptc?.caption
const keywords = (iptc?.keywords as string[])?.join(`\n - `) const keywords = (iptc?.keywords as string[])?.join('\n - ')
const folderName = `${dateShort}-${titleSlug}` const folderName = `${dateShort}-${titleSlug}`
const destination = `${dest}/${folderName}` const destination = `${dest}/${folderName}`
postPhotoFile = `${destination}/index.md` postPhotoFile = `${destination}/index.md`
@ -68,7 +68,7 @@ export async function createPhotoPost(
spinner.succeed( spinner.succeed(
`New photo post '${title}' under '${postPhotoFile}' created.` `New photo post '${title}' under '${postPhotoFile}' created.`
) )
} catch (error: any) { } catch (error: unknown) {
spinner.fail((error as Error).message) spinner.fail((error as Error).message)
} }

View File

@ -1,9 +1,9 @@
import { test, expect, describe, afterEach } from 'vitest'
import { createPhotoPost } from './createPhotoPost'
import { promises as fs } from 'node:fs' import { promises as fs } from 'node:fs'
import path from 'node:path' import path from 'node:path'
import type { Ora } from 'ora' import type { Ora } from 'ora'
import { afterEach, describe, expect, test } from 'vitest'
import { createArticlePost } from './createArticlePost' import { createArticlePost } from './createArticlePost'
import { createPhotoPost } from './createPhotoPost'
const destFolder = path.join('.', 'test/__fixtures__/tmp') const destFolder = path.join('.', 'test/__fixtures__/tmp')
@ -67,7 +67,7 @@ describe('npm run new', () => {
.catch(() => false)) .catch(() => false))
expect(fileExists).toBe(true) expect(fileExists).toBe(true)
expect(spinner.text).toContain(`New photo post`) expect(spinner.text).toContain('New photo post')
// Compare the generated index.md with the fixture new-photo.md // Compare the generated index.md with the fixture new-photo.md
const generatedContent = const generatedContent =

View File

@ -1,7 +1,7 @@
import path from 'node:path' import path from 'node:path'
import ora from 'ora' import ora from 'ora'
import { createPhotoPost } from './createPhotoPost.js'
import { createArticlePost } from './createArticlePost.js' import { createArticlePost } from './createArticlePost.js'
import { createPhotoPost } from './createPhotoPost.js'
const postsPath = path.join('.', 'content', 'articles') const postsPath = path.join('.', 'content', 'articles')
const photosPath = path.join('.', 'content', 'photos') const photosPath = path.join('.', 'content', 'photos')

View File

@ -3,7 +3,7 @@ interface Window {
readonly [language: string]: { readonly [language: string]: {
readonly index: lunr.Index readonly index: lunr.Index
readonly store: { readonly store: {
readonly [key: string]: any readonly [key: string]: unknown
} }
} }
} }

View File

@ -1,5 +1,5 @@
--- ---
import { ChevronLeft } from '@images/components' import { ChevronLeft } from '@/images/components'
--- ---
<style> <style>

View File

@ -1,6 +1,6 @@
--- ---
import { markdownToHtml } from '@lib/markdown' import { getRepo } from '@/lib/github'
import { getRepo } from '@lib/github' import { markdownToHtml } from '@/lib/markdown'
import styles from './index.module.css' import styles from './index.module.css'
type Props = { type Props = {

View File

@ -1,5 +1,5 @@
--- ---
import { Copy as CopyIcon } from '@images/components' import { Copy as CopyIcon } from '@/images/components'
type Props = { type Props = {
text: string text: string

View File

@ -1,5 +1,5 @@
--- ---
import Copy from '@components/Copy.astro' import Copy from '@/components/Copy.astro'
type Props = { type Props = {
address: string address: string

View File

@ -1,8 +1,10 @@
--- ---
import type React from 'react'
type Props = { type Props = {
title: string title: string
value: string value: string
icon: any icon: React.FunctionComponent
} }
const { title, value, icon } = Astro.props const { title, value, icon } = Astro.props

View File

@ -1,5 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, it } from 'vitest' import { describe, it } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import ExifMap from './ExifMap' import ExifMap from './ExifMap'
describe('ExifMap', () => { describe('ExifMap', () => {

View File

@ -1,10 +1,12 @@
import { type ReactElement, useState, useEffect } from 'react' import type { Gps } from '@/lib/exif'
import { Map, Marker } from 'pigeon-maps' import { Marker, Map as PigeonMap } from 'pigeon-maps'
import { type ReactElement, useEffect, useState } from 'react'
const mapbox = const mapbox =
(mapboxId: string) => (x: string, y: string, z: string, dpr: number) => (mapboxId: string) =>
(x: number, y: number, z: number, dpr: number | undefined) =>
`https://api.mapbox.com/styles/v1/mapbox/${mapboxId}/tiles/256/${z}/${x}/${y}${ `https://api.mapbox.com/styles/v1/mapbox/${mapboxId}/tiles/256/${z}/${x}/${y}${
dpr >= 2 ? '@2x' : '' dpr && dpr >= 2 ? '@2x' : ''
}?access_token=${import.meta.env.PUBLIC_MAPBOX_ACCESS_TOKEN}` }?access_token=${import.meta.env.PUBLIC_MAPBOX_ACCESS_TOKEN}`
const providers = { const providers = {
@ -14,11 +16,7 @@ const providers = {
type Theme = 'light' | 'dark' type Theme = 'light' | 'dark'
export default function ExifMap({ export default function ExifMap({ gps }: { gps: Gps }): ReactElement {
gps
}: {
gps: { latitude: number; longitude: number }
}): ReactElement {
const theme = document?.documentElement?.getAttribute('data-theme') as Theme const theme = document?.documentElement?.getAttribute('data-theme') as Theme
const [zoom, setZoom] = useState(12) const [zoom, setZoom] = useState(12)
@ -29,6 +27,7 @@ export default function ExifMap({
setProvider(() => providers[theme]) setProvider(() => providers[theme])
} }
// biome-ignore lint/correctness/useExhaustiveDependencies: handleThemeChange not needed in deps
useEffect(() => { useEffect(() => {
if (!window) return if (!window) return
@ -41,17 +40,17 @@ export default function ExifMap({
const { latitude, longitude } = gps const { latitude, longitude } = gps
return ( return (
<Map <PigeonMap
center={[latitude, longitude]} center={[latitude, longitude]}
zoom={zoom} zoom={zoom}
height={220} height={220}
dprs={[1, 2]} dprs={[1, 2]}
attribution={false} attribution={false}
provider={provider as any} provider={provider}
metaWheelZoom={true} metaWheelZoom={true}
metaWheelZoomWarning={'META+wheel to zoom'} metaWheelZoomWarning={'META+wheel to zoom'}
> >
<Marker anchor={[latitude, longitude]} payload={1} onClick={zoomIn} /> <Marker anchor={[latitude, longitude]} payload={1} onClick={zoomIn} />
</Map> </PigeonMap>
) )
} }

View File

@ -1,16 +1,16 @@
--- ---
import type { Exif } from '@lib/exif'
import { import {
Aperture,
Camera, Camera,
Crosshair, Crosshair,
Aperture, Maximize,
Stopwatch, Stopwatch,
Sun, Sun
Maximize } from '@/images/components'
} from '@images/components' import type { Exif } from '@/lib/exif'
import ExifData from './ExifData.astro' import ExifData from './ExifData.astro'
import styles from './index.module.css'
import ExifMap from './ExifMap.tsx' import ExifMap from './ExifMap.tsx'
import styles from './index.module.css'
type Props = { type Props = {
exif: Exif exif: Exif

View File

@ -47,8 +47,8 @@
} }
.map { .map {
composes: breakout from '@layouts/Base/index.module.css'; composes: breakout from '@/layouts/Base/index.module.css';
composes: frame from '@components/Picture/index.module.css'; composes: frame from '@/components/Picture/index.module.css';
height: 220px; height: 220px;
margin-top: calc(var(--spacer) * 2); margin-top: calc(var(--spacer) * 2);
} }

View File

@ -1,6 +1,6 @@
--- ---
import { Github, Jsonfeed, Mastodon, Rss } from '@/images/components'
import styles from './Networks.module.css' import styles from './Networks.module.css'
import { Rss, Mastodon, Github, Jsonfeed } from '@images/components'
type Props = { type Props = {
links: string[] links: string[]

View File

@ -1,9 +1,9 @@
--- ---
import { Image } from 'astro:assets' import { Image } from 'astro:assets'
import Networks from './Networks.astro' import avatar from '@/images/avatar.jpg'
import Location from '../Location'
import avatar from '@images/avatar.jpg'
import config from '@config/blog.config' import config from '@config/blog.config'
import Location from '../Location'
import Networks from './Networks.astro'
import styles from './Vcard.module.css' import styles from './Vcard.module.css'
const { author, rss, jsonfeed } = config const { author, rss, jsonfeed } = config

View File

@ -1,5 +1,5 @@
.avatar { .avatar {
composes: frame from '@components/Picture/index.module.css'; composes: frame from '@/components/Picture/index.module.css';
border: 2px solid transparent; border: 2px solid transparent;
border-radius: 50% !important; border-radius: 50% !important;
margin-bottom: calc(var(--spacer) / 3); margin-bottom: calc(var(--spacer) / 3);

View File

@ -1,8 +1,8 @@
--- ---
import { Github, Bitcoin } from '@images/components' import { Bitcoin, Github } from '@/images/components'
import config from '@config/blog.config'
import Vcard from './Vcard.astro' import Vcard from './Vcard.astro'
import styles from './index.module.css' import styles from './index.module.css'
import config from '@config/blog.config'
const year = new Date().getFullYear() const year = new Date().getFullYear()
const { name, url, github } = config.author const { name, url, github } = config.author

View File

@ -1,8 +1,8 @@
--- ---
import Menu from '@components/Menu/index.astro' import Menu from '@/components/Menu/index.astro'
import Search from '@features/Search/index.astro' import ThemeSwitch from '@/components/ThemeSwitch/index.astro'
import ThemeSwitch from '@components/ThemeSwitch/index.astro' import Search from '@/features/Search/index.astro'
import { Logo } from '@images/components' import { Logo } from '@/images/components'
import styles from './index.module.css' import styles from './index.module.css'
--- ---

View File

@ -27,7 +27,7 @@
.title { .title {
display: block; display: block;
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */ /* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
font: 0/0 a; font: 0 / 0 a;
color: transparent; color: transparent;
text-shadow: none; text-shadow: none;
background-color: transparent; background-color: transparent;

View File

@ -1,4 +1,4 @@
import { type InputHTMLAttributes, type ReactElement } from 'react' import type { InputHTMLAttributes, ReactElement } from 'react'
import styles from './index.module.css' import styles from './index.module.css'
export default function Input({ export default function Input({

View File

@ -1,8 +1,8 @@
--- ---
import PostTitle from '@layouts/Post/Title.astro'
import styles from './index.module.css'
import type { CollectionEntry } from 'astro:content' import type { CollectionEntry } from 'astro:content'
import LinkActions from '@layouts/Post/LinkActions.astro' import LinkActions from '@/layouts/Post/LinkActions.astro'
import PostTitle from '@/layouts/Post/Title.astro'
import styles from './index.module.css'
type Props = { type Props = {
post: CollectionEntry<'articles' | 'links'> post: CollectionEntry<'articles' | 'links'>

View File

@ -1,5 +1,5 @@
import { Icon as LoaderIcon } from '@/images/components/react/Loader'
import styles from './Loader.module.css' import styles from './Loader.module.css'
import { Icon as LoaderIcon } from '@images/components/react/Loader'
export function Loader() { export function Loader() {
// TODO: fix React props for generated SVG components for class/className // TODO: fix React props for generated SVG components for class/className

View File

@ -1,14 +1,14 @@
import {
describe,
it,
expect,
beforeAll,
afterAll,
vi,
type MockInstance
} from 'vitest'
import { render, screen } from '@testing-library/react'
import * as nanostores from '@nanostores/react' import * as nanostores from '@nanostores/react'
import { render, screen } from '@testing-library/react'
import {
type MockInstance,
afterAll,
beforeAll,
describe,
expect,
it,
vi
} from 'vitest'
import Location from '.' import Location from '.'
const mockData = { const mockData = {

View File

@ -1,5 +1,5 @@
import { $location } from '@/stores/location'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $location } from '@stores/location'
import { formatDistanceToNowStrict } from 'date-fns' import { formatDistanceToNowStrict } from 'date-fns'
import { LocationItem } from './LocationItem' import { LocationItem } from './LocationItem'
import styles from './index.module.css' import styles from './index.module.css'

View File

@ -1,5 +1,5 @@
--- ---
import Hamburger from '@components/Hamburger.astro' import Hamburger from '@/components/Hamburger.astro'
import config from '@config/blog.config' import config from '@config/blog.config'
import styles from './index.module.css' import styles from './index.module.css'

View File

@ -1,5 +1,5 @@
--- ---
import { ChevronRight } from '@images/components' import { ChevronRight } from '@/images/components'
type Props = { type Props = {
href: string href: string

View File

@ -1,6 +1,6 @@
--- ---
import { ChevronLeft, ChevronRight } from '@/images/components'
import styles from './index.module.css' import styles from './index.module.css'
import { ChevronLeft, ChevronRight } from '@images/components'
type Props = { type Props = {
prevPagePath?: string prevPagePath?: string

View File

@ -1,6 +1,6 @@
--- ---
import type { CollectionEntry } from 'astro:content' import type { CollectionEntry } from 'astro:content'
import Picture from '@components/Picture/index.astro' import Picture from '@/components/Picture/index.astro'
type Props = { post: CollectionEntry<'photos'> } type Props = { post: CollectionEntry<'photos'> }

View File

@ -1,7 +1,7 @@
--- ---
import { getImage, type ImgAttributes } from 'astro:assets' import { type ImgAttributes, getImage } from 'astro:assets'
import styles from './index.module.css'
import type { ImageMetadata, ImageOutputFormat } from 'astro' import type { ImageMetadata, ImageOutputFormat } from 'astro'
import styles from './index.module.css'
type Props = ImgAttributes & { type Props = ImgAttributes & {
src: ImageMetadata src: ImageMetadata

View File

@ -1,8 +1,8 @@
--- ---
import PostTitle from '@layouts/Post/Title.astro'
import styles from './index.module.css'
import Picture from '@components/Picture/index.astro'
import type { CollectionEntry } from 'astro:content' import type { CollectionEntry } from 'astro:content'
import Picture from '@/components/Picture/index.astro'
import PostTitle from '@/layouts/Post/Title.astro'
import styles from './index.module.css'
type Props = { type Props = {
post: CollectionEntry<'articles' | 'links'> post: CollectionEntry<'articles' | 'links'>

View File

@ -33,7 +33,7 @@
} }
.empty { .empty {
composes: frame from '@components/Picture/index.module.css'; composes: frame from '@/components/Picture/index.module.css';
display: block; display: block;
min-height: 95px; min-height: 95px;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAACaADAAQAAAABAAAACQAAAAAvQpmhAAAAHElEQVQYGWNgoBL4T8gcggoIGcBA0ASCCmhsBQBhFwX7u70C8QAAAABJRU5ErkJggg=='); background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAACaADAAQAAAABAAAACQAAAAAvQpmhAAAAHElEQVQYGWNgoBL4T8gcggoIGcBA0ASCCmhsBQBhFwX7u70C8QAAAABJRU5ErkJggg==');

View File

@ -1,10 +1,10 @@
--- ---
import type { CollectionEntry } from 'astro:content' import type { CollectionEntry } from 'astro:content'
import PhotoTeaser from '@/components/PhotoTeaser.astro'
import PostTeaser from '@/components/PostTeaser/index.astro'
import { getAllPostsForSearch } from '@/lib/astro'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import styles from './index.module.css' import styles from './index.module.css'
import PhotoTeaser from '@components/PhotoTeaser.astro'
import PostTeaser from '@components/PostTeaser/index.astro'
import { getAllPostsForSearch } from '@lib/astro'
type Props = { type Props = {
post: CollectionEntry<'articles' | 'photos' | 'links'> post: CollectionEntry<'articles' | 'photos' | 'links'>

View File

@ -3,7 +3,7 @@ type Props = {
name: string name: string
url: string url: string
count?: number count?: number
style?: any style?: string | astroHTML.JSX.CSSProperties | null | undefined
} }
const { name, url, count, style } = Astro.props const { name, url, count, style } = Astro.props

View File

@ -1,5 +1,5 @@
--- ---
import { Sun, Moon } from '@images/components' import { Moon, Sun } from '@/images/components'
import styles from './index.module.css' import styles from './index.module.css'
--- ---

View File

@ -1,8 +1,8 @@
import { test, expect, beforeAll } from 'vitest' import { beforeAll, expect, test } from 'vitest'
function resetDocument() { function resetDocument() {
globalThis.localStorage = { globalThis.localStorage = {
getItem: () => undefined, getItem: () => null,
setItem: () => {} setItem: () => {}
} as any } as any
@ -63,11 +63,14 @@ test('data-theme attribute is set based on system preference', async () => {
}) })
test('data-theme attribute changes on system preference change', async () => { test('data-theme attribute changes on system preference change', async () => {
let changeCallback: any = () => {} let changeCallback: ({ matches }: { matches: boolean }) => void = () => {}
globalThis.window.matchMedia = () => globalThis.window.matchMedia = () =>
({ ({
matches: false, matches: false,
addEventListener: (_: any, callback: any) => { addEventListener: (
_: any,
callback: ({ matches }: { matches: boolean }) => void
) => {
changeCallback = callback changeCallback = callback
} }
}) as any }) as any

View File

@ -1,6 +1,6 @@
import { type ReactElement } from 'react' import type { ReactElement } from 'react'
import styles from './Empty.module.css'
import type { Post } from '../Search' import type { Post } from '../Search'
import styles from './Empty.module.css'
const SearchResultsEmpty = ({ const SearchResultsEmpty = ({
query, query,

View File

@ -10,7 +10,7 @@
} }
.results { .results {
composes: content from '@layouts/Base/index.module.css'; composes: content from '@/layouts/Base/index.module.css';
padding: var(--spacer) calc(var(--spacer) / 2); padding: var(--spacer) calc(var(--spacer) / 2);
margin-bottom: 0; margin-bottom: 0;
display: flex; display: flex;
@ -19,11 +19,11 @@
} }
.post { .post {
composes: post from '@components/PostTeaser/index.module.css'; composes: post from '@/components/PostTeaser/index.module.css';
} }
.title { .title {
composes: title from '@components/PostTeaser/index.module.css'; composes: title from '@/components/PostTeaser/index.module.css';
} }
.results li { .results li {

View File

@ -1,8 +1,8 @@
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import styles from './index.module.css'
import ResultsEmpty from './Empty'
import type { Post } from '../Search' import type { Post } from '../Search'
import ResultsEmpty from './Empty'
import styles from './index.module.css'
function SearchResultsPure({ function SearchResultsPure({
query, query,

View File

@ -1,12 +1,19 @@
import { test, expect, vi, afterEach, beforeEach } from 'vitest' import { isSearchOpen } from '@/stores/search'
import { render, fireEvent, waitFor, screen, act } from '@testing-library/react' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { isSearchOpen } from '@stores/search' import {
type MockInstance,
afterEach,
beforeEach,
expect,
test,
vi
} from 'vitest'
import Search from './Search' import Search from './Search'
let portalRoot: HTMLDivElement let portalRoot: HTMLDivElement
let unsubscribe: () => void let unsubscribe: () => void
let fetchSpy: any let fetchSpy: MockInstance<GlobalFetch['fetch']>
let originalFetch: any let originalFetch: GlobalFetch['fetch']
let storeState = false let storeState = false
beforeEach(() => { beforeEach(() => {
@ -34,7 +41,7 @@ beforeEach(() => {
afterEach(() => { afterEach(() => {
portalRoot.remove() portalRoot.remove()
unsubscribe() unsubscribe()
globalThis.fetch = originalFetch ;(globalThis.fetch as GlobalFetch['fetch']) = originalFetch
}) })
test('Search component', async () => { test('Search component', async () => {

View File

@ -1,11 +1,11 @@
import { type ReactElement, useEffect, useState } from 'react' import type { CollectionEntry } from 'astro:content'
import Fuse from 'fuse.js' import Input from '@/components/Input'
import { isSearchOpen } from '@/stores/search'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { isSearchOpen } from '@stores/search' import Fuse from 'fuse.js'
import { type ReactElement, useEffect, useState } from 'react'
import SearchResults from './Results' import SearchResults from './Results'
import styles from './Search.module.css' import styles from './Search.module.css'
import type { CollectionEntry } from 'astro:content'
import Input from '@components/Input'
export type Post = CollectionEntry<'articles' | 'links' | 'photos'> export type Post = CollectionEntry<'articles' | 'links' | 'photos'>
@ -37,6 +37,7 @@ export default function Search(): ReactElement {
// Handle search and set results // Handle search and set results
const fuse = allPosts ? new Fuse(allPosts, fuseOptions) : null const fuse = allPosts ? new Fuse(allPosts, fuseOptions) : null
// biome-ignore lint/correctness/useExhaustiveDependencies: fuse not needed
useEffect(() => { useEffect(() => {
if (!query || query === '' || !fuse) { if (!query || query === '' || !fuse) {
setResults([]) setResults([])
@ -73,6 +74,7 @@ export default function Search(): ReactElement {
onClick={toggleSearch} onClick={toggleSearch}
title="Close search" title="Close search"
> >
{/* biome-ignore lint/a11y/noSvgWithoutTitle: the button has title already */}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" width="24"

View File

@ -1,5 +1,5 @@
--- ---
import { Search as SearchIcon } from '@images/components' import { Search as SearchIcon } from '@/images/components'
import Search from './Search.tsx' import Search from './Search.tsx'
--- ---
@ -10,7 +10,7 @@ import Search from './Search.tsx'
<Search client:only="react" /> <Search client:only="react" />
<script> <script>
import { isSearchOpen } from '@stores/search' import { isSearchOpen } from '@/stores/search'
const button = document.querySelector('#search-button') const button = document.querySelector('#search-button')
isSearchOpen.subscribe((value) => isSearchOpen.subscribe((value) =>

View File

@ -1,8 +1,7 @@
import { useEffect, type ReactElement, useState } from 'react' import { $amount, $selectedToken } from '@/features/Web3/stores'
import styles from './Conversion.module.css'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $selectedToken } from '@features/Web3/stores/selectedToken' import { type ReactElement, useEffect, useState } from 'react'
import { $amount } from '@features/Web3/stores' import styles from './Conversion.module.css'
export function Conversion(): ReactElement | null { export function Conversion(): ReactElement | null {
const selectedToken = useStore($selectedToken) const selectedToken = useStore($selectedToken)

View File

@ -1,5 +1,5 @@
import { test, expect } from 'vitest' import { fireEvent, render, screen } from '@testing-library/react'
import { render, fireEvent, screen } from '@testing-library/react' import { expect, test } from 'vitest'
import { Web3Form } from './Form' import { Web3Form } from './Form'
test('Web3Donation component', async () => { test('Web3Donation component', async () => {

View File

@ -1,17 +1,17 @@
import {
$amount,
$isInitSend,
$selectedToken,
$setAmount
} from '@/features/Web3/stores'
import siteConfig from '@config/blog.config'
import { useStore } from '@nanostores/react'
import { type ReactElement, useEffect, useState } from 'react' import { type ReactElement, useEffect, useState } from 'react'
import { useAccount } from 'wagmi' import { useAccount } from 'wagmi'
import { InputGroup } from '../Input' import { InputGroup } from '../Input'
import styles from './Form.module.css'
import { useStore } from '@nanostores/react'
import {
$selectedToken,
$isInitSend,
$amount,
$setAmount
} from '@features/Web3/stores'
import siteConfig from '@config/blog.config'
import { Send } from '../Send'
import { RainbowKit } from '../RainbowKit/RainbowKit' import { RainbowKit } from '../RainbowKit/RainbowKit'
import { Send } from '../Send'
import styles from './Form.module.css'
export function Web3Form(): ReactElement { export function Web3Form(): ReactElement {
const { address: account } = useAccount() const { address: account } = useAccount()

View File

@ -1,5 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react' import { fireEvent, render, screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest' import { describe, expect, it } from 'vitest'
import { InputGroup } from '.' import { InputGroup } from '.'
describe('InputGroup', () => { describe('InputGroup', () => {

View File

@ -1,15 +1,15 @@
import { useState, type ReactElement } from 'react' import Input from '@/components/Input'
import Input from '@components/Input'
import { Conversion } from '../Conversion'
import styles from './InputGroup.module.css'
import { TokenSelect } from '../TokenSelect'
import { import {
$amount, $amount,
$setAmount,
$isInitSend, $isInitSend,
$selectedToken $selectedToken,
} from '@features/Web3/stores' $setAmount
} from '@/features/Web3/stores'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { type ReactElement, useState } from 'react'
import { Conversion } from '../Conversion'
import { TokenSelect } from '../TokenSelect'
import styles from './InputGroup.module.css'
export function InputGroup({ export function InputGroup({
isDisabled, isDisabled,
@ -50,6 +50,7 @@ export function InputGroup({
className={styles.inputInput} className={styles.inputInput}
/> />
<button <button
type="button"
className={`${styles.submit} btn btn-primary`} className={`${styles.submit} btn btn-primary`}
disabled={ disabled={
isDisabled || isDisabled ||

View File

@ -1,8 +1,8 @@
import { truncateAddress } from '@/features/Web3/lib/truncateAddress'
import { $amount, $selectedToken } from '@/features/Web3/stores'
import { useStore } from '@nanostores/react'
import { useAccount, useChains, useEnsName } from 'wagmi' import { useAccount, useChains, useEnsName } from 'wagmi'
import styles from './Data.module.css' import styles from './Data.module.css'
import { useStore } from '@nanostores/react'
import { $amount, $selectedToken } from '@features/Web3/stores'
import { truncateAddress } from '@features/Web3/lib/truncateAddress'
export function Data({ export function Data({
to, to,
@ -61,7 +61,7 @@ export function Data({
<td className={styles.label}>on</td> <td className={styles.label}>on</td>
<td> <td>
<div className="TokenLogo"> <div className="TokenLogo">
<img src={selectedToken?.chainLogo || ''} /> <img src={selectedToken?.chainLogo || ''} alt="Chain" />
</div> </div>
<span className={styles.network}>{networkName}</span> <span className={styles.network}>{networkName}</span>
</td> </td>

View File

@ -1,5 +1,5 @@
import { describe, test, expect } from 'vitest'
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import { describe, expect, test } from 'vitest'
import { Preview } from './Preview' import { Preview } from './Preview'
describe('Preview component', () => { describe('Preview component', () => {

View File

@ -1,9 +1,9 @@
import { Loader } from '@components/Loader' import { Loader } from '@/components/Loader'
import { useSend } from '@features/Web3/hooks/useSend' import { useSend } from '@/features/Web3/hooks/useSend'
import { $isInitSend } from '@features/Web3/stores' import { $isInitSend } from '@/features/Web3/stores'
import siteConfig from '@config/blog.config'
import { useEnsAddress, useEnsName } from 'wagmi' import { useEnsAddress, useEnsName } from 'wagmi'
import { Data } from './Data' import { Data } from './Data'
import siteConfig from '@config/blog.config'
import styles from './Preview.module.css' import styles from './Preview.module.css'
export function Preview() { export function Preview() {
@ -26,6 +26,7 @@ export function Preview() {
<footer className={styles.actions}> <footer className={styles.actions}>
<button <button
type="button"
onClick={async (e) => { onClick={async (e) => {
e?.preventDefault() e?.preventDefault()
await handleSend() await handleSend()
@ -36,6 +37,7 @@ export function Preview() {
{isLoading ? <Loader /> : 'Make it rain'} {isLoading ? <Loader /> : 'Make it rain'}
</button> </button>
<button <button
type="button"
onClick={() => $isInitSend.set(false)} onClick={() => $isInitSend.set(false)}
className="link" className="link"
disabled={isLoading} disabled={isLoading}

View File

@ -1,7 +1,7 @@
import { $txHash } from '@/features/Web3/stores'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $txHash } from '@features/Web3/stores'
import { Success } from '../Success'
import { Preview } from '../Preview' import { Preview } from '../Preview'
import { Success } from '../Success'
export function Send() { export function Send() {
const txHash = useStore($txHash) const txHash = useStore($txHash)

View File

@ -1,4 +1,4 @@
import { $txHash } from '@features/Web3/stores' import { $txHash } from '@/features/Web3/stores'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
export function ExplorerLink({ export function ExplorerLink({

View File

@ -1,5 +1,5 @@
import { describe, test, expect } from 'vitest'
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import { describe, expect, test } from 'vitest'
import { Success } from './Success' import { Success } from './Success'
describe('Success component', () => { describe('Success component', () => {

View File

@ -1,10 +1,11 @@
import { $isInitSend } from '@features/Web3/stores' import { $isInitSend } from '@/features/Web3/stores'
import styles from './Success.module.css'
import { useAccount } from 'wagmi' import { useAccount } from 'wagmi'
import { ExplorerLink } from './ExplorerLink' import { ExplorerLink } from './ExplorerLink'
import styles from './Success.module.css'
const title = `You're amazing, thanks!` const title = `You're amazing, thanks!`
const description = `Your transaction is on its way. You can check the status on` const description =
'Your transaction is on its way. You can check the status on'
export function Success() { export function Success() {
const account = useAccount() const account = useAccount()
@ -23,6 +24,7 @@ export function Success() {
<footer className={styles.actions}> <footer className={styles.actions}>
<button <button
type="button"
onClick={() => $isInitSend.set(false)} onClick={() => $isInitSend.set(false)}
className="btn btn-primary" className="btn btn-primary"
> >

View File

@ -1,9 +1,9 @@
import { forwardRef, type HTMLAttributes } from 'react'
import * as Select from '@radix-ui/react-select'
import { formatCurrency } from '@coingecko/cryptoformat' import { formatCurrency } from '@coingecko/cryptoformat'
import * as Select from '@radix-ui/react-select'
import { type HTMLAttributes, forwardRef } from 'react'
import './Token.css' import './Token.css'
import { Icon as Check } from '@images/components/react/Check' import type { GetToken } from '@/features/Web3/hooks/useFetchTokens'
import type { GetToken } from '@features/Web3/hooks/useFetchTokens' import { Icon as Check } from '@/images/components/react/Check'
interface SelectItemProps extends HTMLAttributes<HTMLDivElement> { interface SelectItemProps extends HTMLAttributes<HTMLDivElement> {
token: GetToken | undefined token: GetToken | undefined
@ -28,7 +28,7 @@ export const Token = forwardRef<HTMLDivElement, SelectItemProps>(
// const hasBalanceAndValue = // const hasBalanceAndValue =
// balance && parseInt(balance) !== 0 && valueInUsd >= 1 // balance && parseInt(balance) !== 0 && valueInUsd >= 1
const hasBalance = balance && parseInt(balance) !== 0 const hasBalance = balance && Number.parseInt(balance) !== 0
return hasBalance ? ( return hasBalance ? (
<Select.Item <Select.Item
@ -46,6 +46,7 @@ export const Token = forwardRef<HTMLDivElement, SelectItemProps>(
width="32" width="32"
height="32" height="32"
className="TokenLogoImage" className="TokenLogoImage"
alt={token?.symbol?.substring(0, 3)}
/> />
) : ( ) : (
token?.symbol?.substring(0, 3) token?.symbol?.substring(0, 3)
@ -55,6 +56,7 @@ export const Token = forwardRef<HTMLDivElement, SelectItemProps>(
width="20" width="20"
height="20" height="20"
className="TokenChainLogo" className="TokenChainLogo"
alt="Chain"
/> />
</span> </span>
</Select.ItemText> </Select.ItemText>

View File

@ -1,15 +1,15 @@
import * as Select from '@radix-ui/react-select' import * as Select from '@radix-ui/react-select'
import './TokenSelect.css' import './TokenSelect.css'
import { Token } from './Token' import { Loader } from '@/components/Loader'
import { Icon as ChevronDown } from '@images/components/react/ChevronDown' import { useFetchTokens } from '@/features/Web3/hooks/useFetchTokens'
import { Icon as ChevronsDown } from '@images/components/react/ChevronsDown' import { $selectedToken } from '@/features/Web3/stores'
import { Icon as ChevronsUp } from '@images/components/react/ChevronsUp' import { Icon as ChevronDown } from '@/images/components/react/ChevronDown'
import { useFetchTokens } from '@features/Web3/hooks/useFetchTokens' import { Icon as ChevronsDown } from '@/images/components/react/ChevronsDown'
import { Icon as ChevronsUp } from '@/images/components/react/ChevronsUp'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $selectedToken } from '@features/Web3/stores'
import { Loader } from '@components/Loader'
import { useAccount } from 'wagmi'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useAccount } from 'wagmi'
import { Token } from './Token'
export function TokenSelect() { export function TokenSelect() {
const { address } = useAccount() const { address } = useAccount()
@ -29,6 +29,7 @@ export function TokenSelect() {
// Auto-select native token // Auto-select native token
// when no selection was made yet // when no selection was made yet
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => { useEffect(() => {
if (selectedToken?.address || !tokens || !tokens?.length) return if (selectedToken?.address || !tokens || !tokens?.length) return

View File

@ -1,9 +1,8 @@
import '../lib/polyfills'
import { RainbowKitProvider } from '@rainbow-me/rainbowkit' import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
import { WagmiProvider } from 'wagmi'
import { wagmiConfig, theme } from '../lib/rainbowkit'
import { Web3Form } from './Form'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { WagmiProvider } from 'wagmi'
import { theme, wagmiConfig } from '../lib/rainbowkit'
import { Web3Form } from './Form'
const queryClient = new QueryClient() const queryClient = new QueryClient()

View File

@ -1,5 +1,5 @@
import { test, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import { expect, test, vi } from 'vitest'
import { useFetchTokens } from './useFetchTokens' import { useFetchTokens } from './useFetchTokens'
test('useFetchTokens does not fetch anything when no chain or address are present', async () => { test('useFetchTokens does not fetch anything when no chain or address are present', async () => {

View File

@ -1,4 +1,4 @@
import { test, expect } from 'vitest' import { expect, test } from 'vitest'
import { isUnhelpfulErrorMessage } from './isUnhelpfulErrorMessage' import { isUnhelpfulErrorMessage } from './isUnhelpfulErrorMessage'
test('returns true for unhelpful error messages', () => { test('returns true for unhelpful error messages', () => {

View File

@ -1,6 +1,6 @@
import { test, expect, vi } from 'vitest' import { expect, test, vi } from 'vitest'
import { send } from './send'
import * as wagmiActionsMock from '../../../../../test/__mocks__/wagmi/actions' import * as wagmiActionsMock from '../../../../../test/__mocks__/wagmi/actions'
import { send } from './send'
test('with undefined params', async () => { test('with undefined params', async () => {
const result = await send( const result = await send(

View File

@ -1,8 +1,8 @@
import { parseEther, parseUnits } from 'viem'
import type { UseConfigReturnType } from 'wagmi'
import { sendTransaction, writeContract } from 'wagmi/actions' import { sendTransaction, writeContract } from 'wagmi/actions'
import type { GetToken } from '../useFetchTokens' import type { GetToken } from '../useFetchTokens'
import { parseEther, parseUnits } from 'viem'
import { abiErc20Transfer } from './abiErc20Transfer' import { abiErc20Transfer } from './abiErc20Transfer'
import type { UseConfigReturnType } from 'wagmi'
export async function send( export async function send(
config: UseConfigReturnType, config: UseConfigReturnType,

View File

@ -1,10 +1,10 @@
import { $txHash, $selectedToken, $amount } from '@features/Web3/stores' import { $amount, $selectedToken, $txHash } from '@/features/Web3/stores'
import siteConfig from '@config/blog.config'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { useState } from 'react' import { useState } from 'react'
import { send } from './send'
import { isUnhelpfulErrorMessage } from './isUnhelpfulErrorMessage'
import { useAccount, useConfig, useEnsAddress, useSwitchChain } from 'wagmi' import { useAccount, useConfig, useEnsAddress, useSwitchChain } from 'wagmi'
import siteConfig from '@config/blog.config' import { isUnhelpfulErrorMessage } from './isUnhelpfulErrorMessage'
import { send } from './send'
export function useSend() { export function useSend() {
const selectedToken = useStore($selectedToken) const selectedToken = useStore($selectedToken)

View File

@ -1,4 +1,4 @@
import { test, expect } from 'vitest' import { expect, test } from 'vitest'
import { normalizeAmount } from './normalizeAmount' import { normalizeAmount } from './normalizeAmount'
test('normalizeAmount replaces comma with a period', () => { test('normalizeAmount replaces comma with a period', () => {

View File

@ -1,9 +1,9 @@
export function normalizeAmount(amount: string): string { export function normalizeAmount(amount: string): string {
// Replace comma used as a decimal separator with a period // Replace comma used as a decimal separator with a period
amount = amount.replace(',', '.') let newAmount = amount.replace(',', '.')
// Remove any non-digit or non-decimal characters // Remove any non-digit or non-decimal characters
amount = amount.replace(/[^\d.]/g, '') newAmount = newAmount.replace(/[^\d.]/g, '')
return amount return newAmount
} }

View File

@ -1,7 +0,0 @@
import { Buffer } from 'buffer'
window.global = window.global ?? window
window.Buffer = window.Buffer ?? Buffer
window.process = window.process ?? { env: {} } // Minimal process polyfill
export {}

View File

@ -1,5 +1,5 @@
import { type Theme, getDefaultConfig } from '@rainbow-me/rainbowkit' import { type Theme, getDefaultConfig } from '@rainbow-me/rainbowkit'
import { mainnet, polygon, base, optimism, arbitrum, zora } from 'wagmi/chains' import { arbitrum, base, mainnet, optimism, polygon, zora } from 'wagmi/chains'
const PUBLIC_WALLETCONNECT_ID = import.meta.env.PUBLIC_WALLETCONNECT_ID const PUBLIC_WALLETCONNECT_ID = import.meta.env.PUBLIC_WALLETCONNECT_ID
const isProduction = import.meta.env.PROD const isProduction = import.meta.env.PROD

View File

@ -1,4 +1,4 @@
import { test, expect } from 'vitest' import { expect, test } from 'vitest'
import { truncateAddress } from './truncateAddress' import { truncateAddress } from './truncateAddress'
test('truncateAddress', () => { test('truncateAddress', () => {

View File

@ -1,11 +1,11 @@
--- ---
import LayoutBase from '@layouts/Base/index.astro'
import type { Page } from 'astro'
import type { CollectionEntry } from 'astro:content' import type { CollectionEntry } from 'astro:content'
import PostTeaser from '@components/PostTeaser/index.astro' import LinkTeaser from '@/components/LinkTeaser/index.astro'
import Pagination from '@components/Pagination/index.astro' import Pagination from '@/components/Pagination/index.astro'
import PhotoTeaser from '@components/PhotoTeaser.astro' import PhotoTeaser from '@/components/PhotoTeaser.astro'
import LinkTeaser from '@components/LinkTeaser/index.astro' import PostTeaser from '@/components/PostTeaser/index.astro'
import LayoutBase from '@/layouts/Base/index.astro'
import type { Page } from 'astro'
type Props = { type Props = {
page: Page<CollectionEntry<'articles' | 'links' | 'photos'>> page: Page<CollectionEntry<'articles' | 'links' | 'photos'>>

View File

@ -1,11 +1,11 @@
--- ---
import { getImage } from 'astro:assets' import { getImage } from 'astro:assets'
import SchemaOrg, { type Props as SchemaProps } from './SchemaOrg.astro' import faviconSrc from '@/images/favicon.png'
import faviconSvgSrc from '@/images/favicon.svg'
import { getUmamiConfig } from '@/lib/umami'
import config from '@config/blog.config' import config from '@config/blog.config'
import { getUmamiConfig } from '@lib/umami' import SchemaOrg, { type Props as SchemaProps } from './SchemaOrg.astro'
import faviconSrc from '@images/favicon.png' import type { Props as IndexProps } from './index.astro'
import faviconSvgSrc from '@images/favicon.svg'
import { type Props as IndexProps } from './index.astro'
type Props = IndexProps type Props = IndexProps
@ -31,7 +31,7 @@ const imageFinal = `${Astro.site?.origin}${
: faviconSrc.src : faviconSrc.src
}` }`
const imageFinalAlt = image ? `Teaser image for ${title}` : `Logo` const imageFinalAlt = image ? `Teaser image for ${title}` : 'Logo'
const schema: SchemaProps = { const schema: SchemaProps = {
title: titleFinal, title: titleFinal,

View File

@ -2,12 +2,12 @@
import '../../styles/global.css' import '../../styles/global.css'
import '../../styles/imports.css' import '../../styles/imports.css'
import type { ImageMetadata } from 'astro'
import type { CollectionEntry } from 'astro:content' import type { CollectionEntry } from 'astro:content'
import Header from '@components/Header/index.astro' import Footer from '@/components/Footer/index.astro'
import Footer from '@components/Footer/index.astro' import Header from '@/components/Header/index.astro'
import styles from './index.module.css' import type { ImageMetadata } from 'astro'
import Head from './Head.astro' import Head from './Head.astro'
import styles from './index.module.css'
export type Props = CollectionEntry<'articles' | 'links' | 'photos'>['data'] & { export type Props = CollectionEntry<'articles' | 'links' | 'photos'>['data'] & {
pageTitle?: string pageTitle?: string

View File

@ -16,9 +16,7 @@
background-color: var(--body-background-color); background-color: var(--body-background-color);
padding-bottom: calc(var(--spacer) * 2); padding-bottom: calc(var(--spacer) * 2);
border-top: 1px solid rgba(255 255 255 / 85%); border-top: 1px solid rgba(255 255 255 / 85%);
box-shadow: box-shadow: 0 1px 10px rgba(1 85 101 / 10%), 0 -1px 4px rgba(1 85 101 / 5%);
0 1px 10px rgba(1 85 101 / 10%),
0 -1px 4px rgba(1 85 101 / 5%);
/* animates the page menu opening/closing */ /* animates the page menu opening/closing */
transition: 0.2s ease-out; transition: 0.2s ease-out;
@ -32,9 +30,7 @@
[data-theme='dark'] .document { [data-theme='dark'] .document {
border-top-color: rgba(255 255 255 / 5%); border-top-color: rgba(255 255 255 / 5%);
box-shadow: box-shadow: 0 1px 8px rgba(0 7 8 / 30%), 0 -1px 4px rgba(0 21 25 / 80%);
0 1px 8px rgba(0 7 8 / 30%),
0 -1px 4px rgba(0 21 25 / 80%);
} }
@media (min-width: 40rem) and (min-height: 500px) { @media (min-width: 40rem) and (min-height: 500px) {

View File

@ -1,8 +1,10 @@
--- ---
import type React from 'react'
type Props = { type Props = {
title: string title: string
text: string text: string
icon: any icon: React.FunctionComponent
url?: string url?: string
} }

View File

@ -1,8 +1,8 @@
--- ---
import { Bitcoin, Github, Mastodon } from '@/images/components'
import config from '@config/blog.config' import config from '@config/blog.config'
import styles from './Actions.module.css'
import Action from './Action.astro' import Action from './Action.astro'
import { Mastodon, Bitcoin, Github } from '@images/components' import styles from './Actions.module.css'
type Props = { type Props = {
githubLink: string githubLink: string

View File

@ -1,5 +1,5 @@
--- ---
import Time from '@components/Time.astro' import Time from '@/components/Time.astro'
type Props = { type Props = {
date: Date | undefined date: Date | undefined

View File

@ -1,6 +1,6 @@
--- ---
import { ExternalLink, Link } from '@/images/components'
import styles from './LinkActions.module.css' import styles from './LinkActions.module.css'
import { ExternalLink, Link } from '@images/components'
type Props = { type Props = {
linkurl?: string linkurl?: string

View File

@ -1,10 +1,10 @@
--- ---
import type { CollectionEntry } from 'astro:content' import type { CollectionEntry } from 'astro:content'
import { slugify } from '@lib/slugify' import Tag from '@/components/Tag.astro'
import { slugify } from '@/lib/slugify'
import config from '@config/blog.config' import config from '@config/blog.config'
import Tag from '@components/Tag.astro' import DateComponent from './Date.astro'
import styles from './Meta.module.css' import styles from './Meta.module.css'
import Date from './Date.astro'
type Props = { type Props = {
post: CollectionEntry<'articles' | 'links' | 'photos'> post: CollectionEntry<'articles' | 'links' | 'photos'>
@ -22,7 +22,7 @@ const { date, updated, author, tags } = data
</a> </a>
</div> </div>
<Date date={date} updated={updated} /> <DateComponent date={date} updated={updated} />
{ {
collection === 'photos' && ( collection === 'photos' && (

View File

@ -1,7 +1,7 @@
--- ---
import Date from './Date.astro' import { ExternalLink } from '@/images/components'
import DateComponent from './Date.astro'
import styles from './Title.module.css' import styles from './Title.module.css'
import { ExternalLink } from '@images/components'
type Props = { type Props = {
linkurl?: string linkurl?: string
@ -30,7 +30,7 @@ const linkHostname = linkurl ? new URL(linkurl).hostname : null
) : ( ) : (
<> <>
<h1 class={`${styles.title} ${className && className}`}>{title}</h1> <h1 class={`${styles.title} ${className && className}`}>{title}</h1>
{date && <Date date={date} updated={updated} />} {date && <DateComponent date={date} updated={updated} />}
</> </>
) )
} }

View File

@ -1,17 +1,17 @@
--- ---
import type { CollectionEntry } from 'astro:content' import type { CollectionEntry } from 'astro:content'
import LayoutBase from '@layouts/Base/index.astro' import Changelog from '@/components/Changelog/index.astro'
import Title from './Title.astro' import Exif from '@/components/Exif/index.astro'
import Picture from '@/components/Picture/index.astro'
import RelatedPosts from '@/components/RelatedPosts/index.astro'
import Toc from '@/components/Toc.astro'
import LayoutBase from '@/layouts/Base/index.astro'
import type { Exif as ExifType } from '@/lib/exif'
import Actions from './Actions.astro' import Actions from './Actions.astro'
import styles from './index.module.css'
import Meta from './Meta.astro'
import Picture from '@components/Picture/index.astro'
import Toc from '@components/Toc.astro'
import Exif from '@components/Exif/index.astro'
import type { Exif as ExifType } from '@lib/exif'
import Changelog from '@components/Changelog/index.astro'
import RelatedPosts from '@components/RelatedPosts/index.astro'
import LinkActions from './LinkActions.astro' import LinkActions from './LinkActions.astro'
import Meta from './Meta.astro'
import Title from './Title.astro'
import styles from './index.module.css'
type Props = CollectionEntry<'articles' | 'links' | 'photos'> & { type Props = CollectionEntry<'articles' | 'links' | 'photos'> & {
lead?: string // comes in through remark plugin as html lead?: string // comes in through remark plugin as html

View File

@ -42,7 +42,7 @@
.imageWrapper { .imageWrapper {
display: flex; display: flex;
justify-content: center; justify-content: center;
composes: breakout from '@layouts/Base/index.module.css'; composes: breakout from '@/layouts/Base/index.module.css';
} }
.image { .image {

View File

@ -1,9 +1,9 @@
import { test, expect, vi, describe, beforeEach, afterEach } from 'vitest'
import * as loadAndFormatCollectionModule from './loadAndFormatCollection'
import { getAllPosts } from './getAllPosts'
import mockArticles from '@test/__fixtures__/getCollectionArticles.json' import mockArticles from '@test/__fixtures__/getCollectionArticles.json'
import mockLinks from '@test/__fixtures__/getCollectionLinks.json' import mockLinks from '@test/__fixtures__/getCollectionLinks.json'
import mockPhotos from '@test/__fixtures__/getCollectionPhotos.json' import mockPhotos from '@test/__fixtures__/getCollectionPhotos.json'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { getAllPosts } from './getAllPosts'
import * as loadAndFormatCollectionModule from './loadAndFormatCollection'
let loadAndFormatCollectionSpy: any let loadAndFormatCollectionSpy: any

View File

@ -1,6 +1,6 @@
import { sortPosts } from './sortPosts'
import { loadAndFormatCollection } from './loadAndFormatCollection'
import type { CollectionEntry } from 'astro:content' import type { CollectionEntry } from 'astro:content'
import { loadAndFormatCollection } from './loadAndFormatCollection'
import { sortPosts } from './sortPosts'
export async function getAllPosts(): Promise< export async function getAllPosts(): Promise<
CollectionEntry<'articles' | 'links' | 'photos'>[] CollectionEntry<'articles' | 'links' | 'photos'>[]

View File

@ -1,4 +1,4 @@
import { type CollectionEntry } from 'astro:content' import type { CollectionEntry } from 'astro:content'
import { getAllPosts } from './getAllPosts' import { getAllPosts } from './getAllPosts'
// helps to reduce DOM size // helps to reduce DOM size

View File

@ -1,5 +1,5 @@
import { getAllPosts } from './index'
import { slugifyAll } from '../slugify' import { slugifyAll } from '../slugify'
import { getAllPosts } from './index'
export type AllTags = { export type AllTags = {
name: string name: string
@ -10,8 +10,7 @@ export async function getAllTags(): Promise<AllTags> {
const allPosts = await getAllPosts() const allPosts = await getAllPosts()
const allTagsArray = allPosts const allTagsArray = allPosts
.filter((post) => post.data.tags) .filter((post) => post.data.tags)
.map((post) => post.data.tags) .flatMap((post) => post.data.tags) as string[]
.flat() as string[]
const allTagsArrayCleaned = slugifyAll(allTagsArray) const allTagsArrayCleaned = slugifyAll(allTagsArray)
// Explicitly define the type of tagCounts // Explicitly define the type of tagCounts

Some files were not shown because too many files have changed in this diff Show More