1
0
mirror of https://github.com/kremalicious/portfolio.git synced 2024-12-22 09:13:19 +01:00

Migrate to Biome (#1322)

* migrate to Biome

* package updates

* ci fix

* typing fix
This commit is contained in:
Matthias Kretschmann 2024-07-26 11:45:20 +01:00 committed by GitHub
parent 4eff043f17
commit 7eb0075e6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 444 additions and 4136 deletions

View File

@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

View File

@ -15,6 +15,23 @@ env:
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }} NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
jobs: jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run prebuild
- run: npm run lint
- run: npm run typecheck
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -28,6 +45,7 @@ jobs:
cache: 'npm' cache: 'npm'
- run: npm ci - run: npm ci
- run: npm run prebuild
- run: npm test - run: npm test
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4

View File

@ -1,19 +0,0 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"tabWidth": 2,
"endOfLine": "lf",
"plugins": ["@trivago/prettier-plugin-sort-imports"],
"importOrder": [
"^(react/(.*)$)|^(react$)",
"^(next/(.*)$)|^(next$)",
"<THIRD_PARTY_MODULES>",
"^(@/(.*)$)|^(@$)",
"^(@content/(.*)$)|^(@content$)",
"^(@generated/(.*)$)|^(@generated$)",
"^[./]"
],
"importOrderSeparation": false,
"importOrderSortSpecifiers": true
}

View File

@ -1,5 +0,0 @@
{
"extends": [
"stylelint-prettier/recommended"
]
}

View File

@ -104,8 +104,8 @@ If you want to know how, have a look at the respective component:
All SVG assets will be converted to React components with the help of [@svgr/webpack](https://react-svgr.com). Makes use of [SVGR](https://github.com/smooth-code/svgr) so SVG assets can be imported like so: All SVG assets will be converted to React components with the help of [@svgr/webpack](https://react-svgr.com). Makes use of [SVGR](https://github.com/smooth-code/svgr) so SVG assets can be imported like so:
```js ```js
import Logo from './images/logo.svg' import Logo from "./images/logo.svg";
return <Logo /> return <Logo />;
``` ```
## 🤓 Scripts ## 🤓 Scripts
@ -159,18 +159,12 @@ npm run dev
### 🔮 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
```
### 👩‍🔬 Testing ### 👩‍🔬 Testing
Test suite is setup with [Jest](https://jestjs.io) and [react-testing-library](https://github.com/kentcdodds/react-testing-library). Test suite is setup with [Jest](https://jestjs.io) and [react-testing-library](https://github.com/kentcdodds/react-testing-library).

23
biome.json Normal file
View File

@ -0,0 +1,23 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"linter": { "enabled": true, "rules": { "recommended": true } },
"organizeImports": { "enabled": true },
"javascript": {
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingCommas": "none",
"semicolons": "asNeeded",
"arrowParentheses": "always",
"bracketSpacing": true,
"bracketSameLine": false,
"quoteStyle": "single",
"attributePosition": "auto"
}
}
}

View File

@ -40,6 +40,13 @@ const next = (phase, { defaultConfig }) => {
return typeof defaultConfig.webpack === 'function' return typeof defaultConfig.webpack === 'function'
? defaultConfig.webpack(config, options) ? defaultConfig.webpack(config, options)
: config : config
},
eslint: {
// Using Biome instead of ESLint,
// sadly Next.js doesn't have a way to disable ESLint
// see https://github.com/vercel/next.js/discussions/59347
ignoreDuringBuilds: true
} }
} }

4208
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,12 +13,9 @@
"start": "next start", "start": "next start",
"export": "npm run prebuild && next export", "export": "npm run prebuild && next export",
"typecheck": "tsc", "typecheck": "tsc",
"lint:js": "next lint", "lint": "biome check --write src/**/*",
"lint:css": "stylelint ./src/**/*.css",
"lint": "npm run lint:js && npm run lint:css",
"format": "prettier --write 'src/**/*.{ts,tsx,css}'",
"jest": "jest --coverage -c tests/jest.config.js", "jest": "jest --coverage -c tests/jest.config.js",
"test": "NODE_ENV=test npm run prebuild && npm run lint && npm run typecheck && npm run jest", "test": "NODE_ENV=test npm run jest",
"new": "node --import tsx/esm ./scripts/new.ts", "new": "node --import tsx/esm ./scripts/new.ts",
"favicon": "node --import tsx/esm ./scripts/favicon.ts", "favicon": "node --import tsx/esm ./scripts/favicon.ts",
"prebuild": "node --import tsx/esm ./scripts/prebuild.ts" "prebuild": "node --import tsx/esm ./scripts/prebuild.ts"
@ -28,9 +25,9 @@
"@radix-ui/react-select": "^2.1.1", "@radix-ui/react-select": "^2.1.1",
"@yaireo/relative-time": "^1.0.4", "@yaireo/relative-time": "^1.0.4",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"framer-motion": "^11.2.11", "framer-motion": "^11.3.17",
"lucide-react": "^0.399.0", "lucide-react": "^0.416.0",
"next": "14.2.4", "next": "14.2.5",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -39,37 +36,27 @@
"remark-html": "^16.0.1" "remark-html": "^16.0.1"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.8.3",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@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",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.4",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3", "jest-fetch-mock": "^3.0.3",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"ora": "^8.0.1", "ora": "^8.0.1",
"prettier": "^3.3.2",
"sharp": "^0.33.4", "sharp": "^0.33.4",
"sharp-ico": "^0.1.5", "sharp-ico": "^0.1.5",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"stylelint": "^16.6.1", "tsx": "^4.16.2",
"stylelint-prettier": "^5.0.0", "typescript": "^5.5.4"
"tsx": "^4.16.0",
"typescript": "^5.5.2"
}, },
"engines": { "engines": {
"node": "^20.6.0" "node": "^20.6.0"
}, },
"browserslist": [ "browserslist": ["> 0.2%", "last 3 versions", "Firefox ESR", "not dead"]
"> 0.2%",
"last 3 versions",
"Firefox ESR",
"not dead"
]
} }

View File

@ -1 +1,18 @@
{"name":"matthias kretschmann","short_name":"mk","display":"standalone","start_url":"/?homescreen=1","icons":[{"src":"/manifest/favicon-192.png","type":"image/png","sizes":"192x192"},{"src":"/manifest/favicon-512.png","type":"image/png","sizes":"512x512"}]} {
"name": "matthias kretschmann",
"short_name": "mk",
"display": "standalone",
"start_url": "/?homescreen=1",
"icons": [
{
"src": "/manifest/favicon-192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/manifest/favicon-512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}

View File

@ -1,15 +1,13 @@
// https://github.com/NekR/self-destroying-sw // https://github.com/NekR/self-destroying-sw
self.addEventListener('install', function (e) { self.addEventListener('install', (e) => {
self.skipWaiting() self.skipWaiting()
}) })
self.addEventListener('activate', function (e) { self.addEventListener('activate', (e) => {
self.registration self.registration
.unregister() .unregister()
.then(function () { .then(() => self.clients.matchAll())
return self.clients.matchAll() .then((clients) => {
})
.then(function (clients) {
clients.forEach((client) => client.navigate(client.url)) clients.forEach((client) => client.navigate(client.url))
}) })
}) })

View File

@ -1,8 +1,8 @@
import fs from 'fs' import fs from 'node:fs'
import path from 'node:path'
import type { ProjectType } from '@/types/project'
import yaml from 'js-yaml' import yaml from 'js-yaml'
import ora from 'ora' import ora from 'ora'
import path from 'path'
import type ProjectType from '@/types/project'
import { transformProject } from './transformProject' import { transformProject } from './transformProject'
const contentDirectory = path.join(process.cwd(), '_content') const contentDirectory = path.join(process.cwd(), '_content')

View File

@ -1,7 +1,7 @@
import fs from 'fs' import fs from 'node:fs'
import { join } from 'path' import { join } from 'node:path'
import type { ImageType } from '@/types/image'
import sharp from 'sharp' import sharp from 'sharp'
import type ImageType from '@/types/image'
import { rgbDataURL } from './rgbDataURL' import { rgbDataURL } from './rgbDataURL'
const imagesDirectory = join(process.cwd(), 'public', 'images') const imagesDirectory = join(process.cwd(), 'public', 'images')

View File

@ -1,4 +1,4 @@
import ProjectType from '@/types/project' import type { ProjectType } from '@/types/project'
import { getProjectImages } from './images' import { getProjectImages } from './images'
import { markdownToHtml } from './markdown' import { markdownToHtml } from './markdown'

View File

@ -1,7 +1,7 @@
#!/usr/bin/env ts-node #!/usr/bin/env ts-node
import fs from 'fs' import fs from 'fs'
import ora from 'ora'
import path from 'path' import path from 'path'
import ora from 'ora'
import slugify from 'slugify' import slugify from 'slugify'
const templatePath = path.join(process.cwd(), 'scripts', 'new.yml') const templatePath = path.join(process.cwd(), 'scripts', 'new.yml')

View File

@ -1,5 +1,3 @@
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import Header from '@/components/Header/Header' import Header from '@/components/Header/Header'
import Project from '@/components/Project' import Project from '@/components/Project'
import ProjectNav from '@/components/ProjectNav' import ProjectNav from '@/components/ProjectNav'
@ -7,6 +5,8 @@ import { getAllSlugs } from '@/lib/getAllSlugs'
import { getProjectBySlug } from '@/lib/getProjectBySlug' import { getProjectBySlug } from '@/lib/getProjectBySlug'
import meta from '@content/meta.json' import meta from '@content/meta.json'
import projects from '@generated/projects.json' import projects from '@generated/projects.json'
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
type Props = { type Props = {
params: { slug: string } params: { slug: string }
@ -21,10 +21,10 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
description: `${project.description.slice(0, 157)}...`, description: `${project.description.slice(0, 157)}...`,
metadataBase: new URL(meta.url), metadataBase: new URL(meta.url),
alternates: { alternates: {
canonical: '/' + project.slug canonical: `/${project.slug}`
}, },
openGraph: { openGraph: {
url: '/' + project.slug, url: `/${project.slug}`,
images: [{ url: project.images[0].src }] images: [{ url: project.images[0].src }]
} }
} }

View File

@ -1,5 +1,5 @@
import { render } from '@testing-library/react'
import meta from '@content/meta.json' import meta from '@content/meta.json'
import { render } from '@testing-library/react'
import projectMock from '../../../tests/__fixtures__/project.json' import projectMock from '../../../tests/__fixtures__/project.json'
import Page, { generateMetadata, generateStaticParams } from '../[slug]/page' import Page, { generateMetadata, generateStaticParams } from '../[slug]/page'
@ -31,10 +31,10 @@ describe('app: [slug]/page', () => {
description: `${projectMock.description.slice(0, 157)}...`, description: `${projectMock.description.slice(0, 157)}...`,
metadataBase: new URL(meta.url), metadataBase: new URL(meta.url),
alternates: { alternates: {
canonical: '/' + projectMock.slug canonical: `/${projectMock.slug}`
}, },
openGraph: { openGraph: {
url: '/' + projectMock.slug, url: `/${projectMock.slug}`,
images: [{ url: projectMock.images[0].src }] images: [{ url: projectMock.images[0].src }]
} }
}) })

View File

@ -5,10 +5,10 @@ describe('app: /layout', () => {
// suppress error "Warning: validateDOMNesting(...): <html> cannot appear as a child of <div>" // suppress error "Warning: validateDOMNesting(...): <html> cannot appear as a child of <div>"
// https://github.com/testing-library/react-testing-library/issues/1250 // https://github.com/testing-library/react-testing-library/issues/1250
let originalError: { let originalError: {
(...data: any[]): void (...data: unknown[]): void
(message?: any, ...optionalParams: any[]): void (message?: unknown, ...optionalParams: unknown[]): void
(...data: any[]): void (...data: unknown[]): void
(message?: any, ...optionalParams: any[]): void (message?: unknown, ...optionalParams: unknown[]): void
} }
beforeAll(() => { beforeAll(() => {

View File

@ -1,10 +1,10 @@
import { ReactNode } from 'react'
import { Metadata, Viewport } from 'next'
import Script from 'next/script'
import Footer from '@/components/Footer' import Footer from '@/components/Footer'
import HostnameCheck from '@/components/HostnameCheck' import HostnameCheck from '@/components/HostnameCheck'
import ThemeSwitch from '@/components/ThemeSwitch' import ThemeSwitch from '@/components/ThemeSwitch'
import { UMAMI_SCRIPT_URL, UMAMI_WEBSITE_ID } from '@/lib/umami' import { UMAMI_SCRIPT_URL, UMAMI_WEBSITE_ID } from '@/lib/umami'
import type { Metadata, Viewport } from 'next'
import Script from 'next/script'
import type { ReactNode } from 'react'
import '@/styles/global.css' import '@/styles/global.css'
import styles from '@/styles/layout.module.css' import styles from '@/styles/layout.module.css'
import meta from '@content/meta.json' import meta from '@content/meta.json'
@ -50,7 +50,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<link rel="icon" href="/favicon.ico" sizes="any" /> <link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest/manifest.webmanifest"></link> <link rel="manifest" href="/manifest/manifest.webmanifest" />
{isProduction && ( {isProduction && (
<Script <Script

View File

@ -1,8 +1,8 @@
import { Metadata } from 'next'
import NotFound from '@/components/404' import NotFound from '@/components/404'
import type { Metadata } from 'next'
export const metadata: Metadata = { export const metadata: Metadata = {
title: `Shenanigans`, title: 'Shenanigans',
description: 'Page not found.' description: 'Page not found.'
} }

View File

@ -1,10 +1,10 @@
import { Suspense } from 'react'
import Hero from '@/components/Hero' import Hero from '@/components/Hero'
import Projects from '@/components/Projects' import Projects from '@/components/Projects'
import Repositories from '@/components/Repositories/Repositories' import Repositories from '@/components/Repositories/Repositories'
import { preloadLocation } from '@/lib/getLocation' import { preloadLocation } from '@/lib/getLocation'
import { getRepos } from '@/lib/getRepos' import { getRepos } from '@/lib/getRepos'
import projects from '@generated/projects.json' import projects from '@generated/projects.json'
import { Suspense } from 'react'
export default async function IndexPage() { export default async function IndexPage() {
const repos = await getRepos() const repos = await getRepos()

View File

@ -1,5 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react'
import NotFoundPage from '@/components/404' import NotFoundPage from '@/components/404'
import { fireEvent, render, screen } from '@testing-library/react'
import mockData from '../../../tests/__fixtures__/giphy.json' import mockData from '../../../tests/__fixtures__/giphy.json'
describe('NotFoundPage', () => { describe('NotFoundPage', () => {

View File

@ -1,9 +1,9 @@
'use client' 'use client'
import { MouseEvent, useEffect, useState } from 'react' import { getRandomGif } from '@/lib/getRandomGif'
import Link from 'next/link' import Link from 'next/link'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import { getRandomGif } from '@/lib/getRandomGif' import { type MouseEvent, useEffect, useState } from 'react'
import Button from '../Button' import Button from '../Button'
import styles from './index.module.css' import styles from './index.module.css'
@ -36,13 +36,9 @@ export default function NotFound() {
{tag} gifs, entirely your choice. {tag} gifs, entirely your choice.
</p> </p>
<video <video className="gif" src={gif} data-testid={gif || null} autoPlay loop>
className="gif" <track kind="captions" srcLang="en" label="English" />
src={gif} </video>
data-testid={gif || null}
autoPlay
loop
/>
<div> <div>
<Button onClick={handleClick}>{`Get another '${tag}' gif`}</Button> <Button onClick={handleClick}>{`Get another '${tag}' gif`}</Button>

View File

@ -10,6 +10,7 @@ export default function Availability() {
return ( return (
<section className={className}> <section className={className}>
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: nothing injected on run time */}
<p dangerouslySetInnerHTML={{ __html: html }} /> <p dangerouslySetInnerHTML={{ __html: html }} />
</section> </section>
) )

View File

@ -1,3 +1,5 @@
import Farcaster from '@/images/farcaster.svg'
import Mastodon from '@/images/mastodon.svg'
// https://lucide.dev // https://lucide.dev
import { import {
ArrowDownCircle, ArrowDownCircle,
@ -16,8 +18,6 @@ import {
Star, Star,
Sun Sun
} from 'lucide-react' } from 'lucide-react'
import Farcaster from '@/images/farcaster.svg'
import Mastodon from '@/images/mastodon.svg'
import styles from './index.module.css' import styles from './index.module.css'
type Props = React.SVGAttributes<{ type Props = React.SVGAttributes<{

View File

@ -1,13 +1,13 @@
'use client' 'use client'
import { useEffect, useState, useTransition } from 'react' import { getLocation } from '@/lib/getLocation'
import RelativeTime from '@yaireo/relative-time' import RelativeTime from '@yaireo/relative-time'
import { LazyMotion, domAnimation, m, useReducedMotion } from 'framer-motion' import { LazyMotion, domAnimation, m, useReducedMotion } from 'framer-motion'
import { getLocation } from '@/lib/getLocation' import { useEffect, useState, useTransition } from 'react'
import { fadeIn, getAnimationProps } from '../Transitions' import { fadeIn, getAnimationProps } from '../Transitions'
import { Flag } from './Flag' import { Flag } from './Flag'
import styles from './Location.module.css' import styles from './Location.module.css'
import { UseLocation } from './types' import type { UseLocation } from './types'
function Animation({ children }: { children: React.ReactNode }) { function Animation({ children }: { children: React.ReactNode }) {
const shouldReduceMotion = useReducedMotion() const shouldReduceMotion = useReducedMotion()

View File

@ -1,6 +1,6 @@
import Link from 'next/link'
import Logo from '@/images/logo.svg' import Logo from '@/images/logo.svg'
import meta from '@content/meta.json' import meta from '@content/meta.json'
import Link from 'next/link'
import styles from './index.module.css' import styles from './index.module.css'
type Props = { type Props = {

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import { LazyMotion, domAnimation, m, useReducedMotion } from 'framer-motion'
import meta from '@content/meta.json' import meta from '@content/meta.json'
import { LazyMotion, domAnimation, m, useReducedMotion } from 'framer-motion'
import { getAnimationProps } from '../Transitions' import { getAnimationProps } from '../Transitions'
import { NetworkLink } from './NetworkLink' import { NetworkLink } from './NetworkLink'
import styles from './index.module.css' import styles from './index.module.css'

View File

@ -1,11 +1,12 @@
import Button from '@/components/Button' import Button from '@/components/Button'
import Icon from '@/components/Icon' import Icon from '@/components/Icon'
import type { ProjectLink } from '@/types'
import styles from './index.module.css' import styles from './index.module.css'
export default function ProjectLinks({ export default function ProjectLinks({
links links
}: { }: {
links: { title: string; url: string; icon: string }[] links: ProjectLink[]
}) { }) {
return ( return (
<div className={styles.projectLinks}> <div className={styles.projectLinks}>

View File

@ -1,8 +1,7 @@
'use client' 'use client'
import type { ImageType, ProjectType } from '@/types'
import { LazyMotion, domAnimation, m, useReducedMotion } from 'framer-motion' import { LazyMotion, domAnimation, m, useReducedMotion } from 'framer-motion'
import type ImageType from '@/types/image'
import type ProjectType from '@/types/project'
import ProjectImage from '../ProjectImage' import ProjectImage from '../ProjectImage'
import { getAnimationProps, moveInBottom } from '../Transitions' import { getAnimationProps, moveInBottom } from '../Transitions'
import ProjectLinks from './Links' import ProjectLinks from './Links'
@ -41,6 +40,7 @@ export default function Project({
<m.div <m.div
variants={moveInBottom} variants={moveInBottom}
className={styles.description} className={styles.description}
// biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
dangerouslySetInnerHTML={{ __html: descriptionHtml ?? '' }} dangerouslySetInnerHTML={{ __html: descriptionHtml ?? '' }}
/> />
</m.header> </m.header>
@ -51,7 +51,7 @@ export default function Project({
className={styles.fullContainer} className={styles.fullContainer}
image={image} image={image}
alt={`Showcase image no. ${i + 1} for ${title}`} alt={`Showcase image no. ${i + 1} for ${title}`}
key={i} key={image.src}
sizes="100vw" sizes="100vw"
/> />
))} ))}

View File

@ -1,6 +1,7 @@
import type { ImageType } from '@/types'
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import project from '@tests/__fixtures__/project.json'
import ProjectImage from '.' import ProjectImage from '.'
import project from '../../../tests/__fixtures__/project.json'
describe('ProjectImage', () => { describe('ProjectImage', () => {
it('renders correctly', async () => { it('renders correctly', async () => {
@ -17,7 +18,11 @@ describe('ProjectImage', () => {
it('returns without errors without image', async () => { it('returns without errors without image', async () => {
render( render(
<ProjectImage image={null as any} alt={project.title} sizes="100vw" /> <ProjectImage
image={null as unknown as ImageType}
alt={project.title}
sizes="100vw"
/>
) )
}) })
}) })

View File

@ -1,5 +1,5 @@
import type { ImageType } from '@/types'
import Image from 'next/image' import Image from 'next/image'
import ImageType from '@/types/image'
import styles from './index.module.css' import styles from './index.module.css'
export default function ProjectImage({ export default function ProjectImage({

View File

@ -1,6 +1,6 @@
import { ForwardedRef, forwardRef } from 'react'
import Link from 'next/link' import Link from 'next/link'
import ProjectType from '../../types/project' import { type ForwardedRef, forwardRef } from 'react'
import type { ProjectType } from '../../types/project'
import ProjectImage from '../ProjectImage' import ProjectImage from '../ProjectImage'
import styles from './index.module.css' import styles from './index.module.css'

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import type { ProjectType } from '@/types'
import { createRef, useEffect } from 'react' import { createRef, useEffect } from 'react'
import ProjectType from '@/types/project'
import { Project } from './Project' import { Project } from './Project'
import styles from './index.module.css' import styles from './index.module.css'
@ -22,7 +22,7 @@ export default function ProjectNav({ projects, currentSlug }: Props) {
const activeItem = currentItem.current const activeItem = currentItem.current
const scrollRect = scrollContainer.current.getBoundingClientRect() const scrollRect = scrollContainer.current.getBoundingClientRect()
const activeRect = activeItem && activeItem.getBoundingClientRect() const activeRect = activeItem?.getBoundingClientRect()
if (!activeItem || !scrollRect || !activeRect) return if (!activeItem || !scrollRect || !activeRect) return
const newScrollLeftPosition = const newScrollLeftPosition =

View File

@ -1,5 +1,5 @@
import type { ImageType } from '@/types'
import Link from 'next/link' import Link from 'next/link'
import ImageType from '../../types/image'
import ProjectImage from '../ProjectImage' import ProjectImage from '../ProjectImage'
import styles from './index.module.css' import styles from './index.module.css'

View File

@ -1,4 +1,4 @@
import ProjectType from '@/types/project' import type { ProjectType } from '@/types/project'
import ProjectPreview from '../ProjectPreview' import ProjectPreview from '../ProjectPreview'
import styles from './index.module.css' import styles from './index.module.css'

View File

@ -1,7 +1,7 @@
import type { Repo } from '@/types/repo'
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import Repo from '@/types/repo' import repos from '@tests/__fixtures__/repos.json'
import Repositories from '.' import Repositories from '.'
import repos from '../../../tests/__fixtures__/repos.json'
describe('Repositories', () => { describe('Repositories', () => {
it('renders correctly', () => { it('renders correctly', () => {

View File

@ -1,4 +1,4 @@
import Repo from '@/types/repo' import type { Repo } from '@/types'
import Repository from '../Repository' import Repository from '../Repository'
import styles from './Repositories.module.css' import styles from './Repositories.module.css'
@ -7,7 +7,9 @@ export default function Repositories({ repos }: { repos: Repo[] | undefined }) {
<> <>
<h2 className={styles.sectionTitle}>Open Source Projects</h2> <h2 className={styles.sectionTitle}>Open Source Projects</h2>
<div className={styles.repos}> <div className={styles.repos}>
{repos?.map((repo) => <Repository key={repo.name} repo={repo} />)} {repos?.map((repo) => (
<Repository key={repo.name} repo={repo} />
))}
</div> </div>
</> </>
) )

View File

@ -1,7 +1,7 @@
import type { Repo } from '@/types/repo'
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import Repo from '@/types/repo' import repos from '@tests/__fixtures__/repos.json'
import Repository from '.' import Repository from '.'
import repos from '../../../tests/__fixtures__/repos.json'
describe('Repository', () => { describe('Repository', () => {
it('renders correctly', () => { it('renders correctly', () => {

View File

@ -1,4 +1,4 @@
import Repo from '@/types/repo' import type { Repo } from '@/types'
import Icon from '../Icon' import Icon from '../Icon'
import styles from './index.module.css' import styles from './index.module.css'

View File

@ -1,9 +1,9 @@
'use client' 'use client'
import { useEffect, useState } from 'react' import Icon from '@/components/Icon'
import * as Select from '@radix-ui/react-select' import * as Select from '@radix-ui/react-select'
import { useTheme } from 'next-themes' import { useTheme } from 'next-themes'
import Icon from '../Icon' import { useEffect, useState } from 'react'
import { Item } from './Item' import { Item } from './Item'
import styles from './index.module.css' import styles from './index.module.css'
@ -45,7 +45,7 @@ export default function ThemeSwitch() {
<Select.Arrow className={styles.arrow} width={14} height={7} /> <Select.Arrow className={styles.arrow} width={14} height={7} />
<Select.Viewport className={styles.viewport}> <Select.Viewport className={styles.viewport}>
{themes {themes
.map((theme) => <Item key={theme} theme={theme}></Item>) .map((theme) => <Item key={theme} theme={theme} />)
.reverse()} .reverse()}
</Select.Viewport> </Select.Viewport>
</Select.Content> </Select.Content>

View File

@ -1,6 +1,6 @@
import saveAs from 'file-saver'
import avatar from '@/images/avatar.jpg' import avatar from '@/images/avatar.jpg'
import meta from '@content/meta.json' import meta from '@content/meta.json'
import saveAs from 'file-saver'
import { imageToDataUrl } from './imageToDataUrl' import { imageToDataUrl } from './imageToDataUrl'
export function constructVcard(dataUrl: string) { export function constructVcard(dataUrl: string) {

View File

@ -1,4 +1,4 @@
import fetch, { FetchMock } from 'jest-fetch-mock' import fetch, { type FetchMock } from 'jest-fetch-mock'
import { imageToDataUrl } from './imageToDataUrl' import { imageToDataUrl } from './imageToDataUrl'
const dummyPath = 'http://example.com/image.png' const dummyPath = 'http://example.com/image.png'
@ -23,6 +23,7 @@ describe('imageToDataUrl', () => {
}) })
it('should convert image to data URL', async () => { it('should convert image to data URL', async () => {
// biome-ignore lint/suspicious/noExplicitAny: not worth it for mocking
function MockFileReader(this: any) { function MockFileReader(this: any) {
this.readAsDataURL = function () { this.readAsDataURL = function () {
this.result = 'data:image/png;base64,...' this.result = 'data:image/png;base64,...'
@ -30,7 +31,7 @@ describe('imageToDataUrl', () => {
} }
} }
window.FileReader = MockFileReader as any window.FileReader = MockFileReader as unknown as typeof FileReader
const dataUrl = await imageToDataUrl(dummyPath) const dataUrl = await imageToDataUrl(dummyPath)
@ -39,12 +40,12 @@ describe('imageToDataUrl', () => {
it('should handle errors in readAsDataURL', async () => { it('should handle errors in readAsDataURL', async () => {
function MockFileReader(this: FileReader) { function MockFileReader(this: FileReader) {
this.readAsDataURL = function () { this.readAsDataURL = () => {
throw new Error('Mock error') throw new Error('Mock error')
} }
} }
window.FileReader = MockFileReader as any window.FileReader = MockFileReader as unknown as typeof FileReader
// Expect imageToDataUrl to reject with the mock error // Expect imageToDataUrl to reject with the mock error
await expect(imageToDataUrl(dummyPath)).rejects.toThrow('Mock error') await expect(imageToDataUrl(dummyPath)).rejects.toThrow('Mock error')

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import { MouseEvent } from 'react'
import meta from '@content/meta.json' import meta from '@content/meta.json'
import type { MouseEvent } from 'react'
export default function Vcard() { export default function Vcard() {
const handleAddressbookClick = (e: MouseEvent) => { const handleAddressbookClick = (e: MouseEvent) => {

View File

@ -1,7 +1,7 @@
'use server' 'use server'
import { revalidatePath } from 'next/cache'
import { GiphyFetch } from '@giphy/js-fetch-api' import { GiphyFetch } from '@giphy/js-fetch-api'
import { revalidatePath } from 'next/cache'
export async function getRandomGif(tag: string, pathname?: string) { export async function getRandomGif(tag: string, pathname?: string) {
try { try {

View File

@ -1,11 +1,10 @@
import * as React from 'react'
import fetch, { FetchMock } from 'jest-fetch-mock'
import repoFilter from '@content/repos.json' import repoFilter from '@content/repos.json'
import fetch, { type FetchMock } from 'jest-fetch-mock'
import { getRepos } from './getRepos' import { getRepos } from './getRepos'
jest.mock('react', () => ({ jest.mock('react', () => ({
...jest.requireActual('react'), ...jest.requireActual('react'),
cache: (fn: any) => fn cache: (fn: () => void) => fn
})) }))
describe('getRepos', () => { describe('getRepos', () => {
@ -33,8 +32,9 @@ describe('getRepos', () => {
}) })
it('should handle network errors', async () => { it('should handle network errors', async () => {
let consoleErrorMock: jest.SpyInstance const consoleErrorMock: jest.SpyInstance = jest
consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => {}) .spyOn(console, 'error')
.mockImplementation(() => {})
;(fetch as FetchMock).mockRejectOnce(new Error('Network error')) ;(fetch as FetchMock).mockRejectOnce(new Error('Network error'))
const data = await getRepos() const data = await getRepos()

View File

@ -1,8 +1,8 @@
'use server' 'use server'
import { cache } from 'react' import type { Repo } from '@/types'
import type Repo from '@/types/repo'
import filter from '@content/repos.json' import filter from '@content/repos.json'
import { cache } from 'react'
// //
// Get GitHub repos // Get GitHub repos
@ -22,7 +22,7 @@ export const getRepos = cache(async () => {
try { try {
let repos: Repo[] = [] let repos: Repo[] = []
for (let item of filter) { for (const item of filter) {
const user = item.split('/')[0] const user = item.split('/')[0]
const repoName = item.split('/')[1] const repoName = item.split('/')[1]
const response = await fetch( const response = await fetch(

View File

@ -1,9 +1,7 @@
declare type ImageType = { export declare type ImageType = {
src: string src: string
width?: number width?: number
height?: number height?: number
format?: string format?: string
blurDataURL?: string blurDataURL?: string
} }
export default ImageType

3
src/types/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './image'
export * from './project'
export * from './repo'

View File

@ -1,13 +1,17 @@
import ImageType from './image' import type { ImageType } from './image'
declare type ProjectType = { export declare type ProjectLink = {
title: string
url: string
icon?: string
}
export declare type ProjectType = {
images: ImageType[] images: ImageType[]
slug: string slug: string
title: string title: string
description: string description: string
descriptionHtml: string descriptionHtml: string
techstack: string[] techstack: string[]
links?: any links?: ProjectLink[]
} }
export default ProjectType

View File

@ -1,4 +1,4 @@
declare type Repo = { export declare type Repo = {
name: string name: string
full_name: string full_name: string
description: string description: string
@ -7,5 +7,3 @@ declare type Repo = {
stargazers_count: number stargazers_count: number
pushed_at: string pushed_at: string
} }
export default Repo

12
src/types/umami.d.ts vendored
View File

@ -1,17 +1,23 @@
declare global { declare global {
interface Window { interface Window {
umami?: (eventName: string) => void | { umami?: (eventName: string) =>
| undefined
| {
trackEvent: ( trackEvent: (
event_name: string, event_name: string,
event_data?: { [key: string]: string }, event_data?: { [key: string]: string },
url?: string, url?: string,
website_id?: string website_id?: string
) => void ) => void
trackView: (url: string, referrer?: string, website_id?: string) => void trackView: (
url: string,
referrer?: string,
website_id?: string
) => void
} }
} }
} }
window.umami = window.umami || {} window.umami = window.umami || {}
export {} export type {}

1
src/types/yaml.d.ts vendored
View File

@ -1,4 +1,5 @@
declare module '*.yml' { declare module '*.yml' {
// biome-ignore lint/suspicious/noExplicitAny: could be any data
const data: any const data: any
export default data export default data
} }

View File

@ -1,4 +1,4 @@
import { SVGProps, forwardRef } from 'react' import { type SVGProps, forwardRef } from 'react'
const SvgrMock = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>( const SvgrMock = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
(props, ref) => <svg ref={ref} {...props} /> (props, ref) => <svg ref={ref} {...props} />

View File

@ -17,6 +17,7 @@
"plugins": [{ "name": "next" }], "plugins": [{ "name": "next" }],
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"],
"@tests/*": ["./tests/*"],
"@content/*": ["./_content/*"], "@content/*": ["./_content/*"],
"@generated/*": ["./generated/*"] "@generated/*": ["./generated/*"]
} }