1
0
mirror of https://github.com/kremalicious/portfolio.git synced 2024-12-12 12:37:13 +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 }}
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:
runs-on: ubuntu-latest
@ -28,6 +45,7 @@ jobs:
cache: 'npm'
- run: npm ci
- run: npm run prebuild
- run: npm test
- 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:
```js
import Logo from './images/logo.svg'
return <Logo />
import Logo from "./images/logo.svg";
return <Logo />;
```
## 🤓 Scripts
@ -159,18 +159,12 @@ npm run dev
### 🔮 Linting
ESLint, Prettier, and Stylelint are setup for all linting purposes:
[Biome](https://biomejs.dev) is setup for all linting and formatting purposes:
```bash
npm run lint
```
To automatically format all code files:
```bash
npm run format
```
### 👩‍🔬 Testing
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'
? defaultConfig.webpack(config, options)
: 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",
"export": "npm run prebuild && next export",
"typecheck": "tsc",
"lint:js": "next lint",
"lint:css": "stylelint ./src/**/*.css",
"lint": "npm run lint:js && npm run lint:css",
"format": "prettier --write 'src/**/*.{ts,tsx,css}'",
"lint": "biome check --write src/**/*",
"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",
"favicon": "node --import tsx/esm ./scripts/favicon.ts",
"prebuild": "node --import tsx/esm ./scripts/prebuild.ts"
@ -28,9 +25,9 @@
"@radix-ui/react-select": "^2.1.1",
"@yaireo/relative-time": "^1.0.4",
"file-saver": "^2.0.5",
"framer-motion": "^11.2.11",
"lucide-react": "^0.399.0",
"next": "14.2.4",
"framer-motion": "^11.3.17",
"lucide-react": "^0.416.0",
"next": "14.2.5",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -39,37 +36,27 @@
"remark-html": "^16.0.1"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@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",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/file-saver": "^2.0.7",
"@types/jest": "^29.5.12",
"@types/js-yaml": "^4.0.9",
"chalk": "^5.3.0",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.4",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"js-yaml": "^4.1.0",
"ora": "^8.0.1",
"prettier": "^3.3.2",
"sharp": "^0.33.4",
"sharp-ico": "^0.1.5",
"slugify": "^1.6.6",
"stylelint": "^16.6.1",
"stylelint-prettier": "^5.0.0",
"tsx": "^4.16.0",
"typescript": "^5.5.2"
"tsx": "^4.16.2",
"typescript": "^5.5.4"
},
"engines": {
"node": "^20.6.0"
},
"browserslist": [
"> 0.2%",
"last 3 versions",
"Firefox ESR",
"not dead"
]
"browserslist": ["> 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
self.addEventListener('install', function (e) {
self.addEventListener('install', (e) => {
self.skipWaiting()
})
self.addEventListener('activate', function (e) {
self.addEventListener('activate', (e) => {
self.registration
.unregister()
.then(function () {
return self.clients.matchAll()
})
.then(function (clients) {
.then(() => self.clients.matchAll())
.then((clients) => {
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 ora from 'ora'
import path from 'path'
import type ProjectType from '@/types/project'
import { transformProject } from './transformProject'
const contentDirectory = path.join(process.cwd(), '_content')

View File

@ -1,7 +1,7 @@
import fs from 'fs'
import { join } from 'path'
import fs from 'node:fs'
import { join } from 'node:path'
import type { ImageType } from '@/types/image'
import sharp from 'sharp'
import type ImageType from '@/types/image'
import { rgbDataURL } from './rgbDataURL'
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 { markdownToHtml } from './markdown'

View File

@ -1,7 +1,7 @@
#!/usr/bin/env ts-node
import fs from 'fs'
import ora from 'ora'
import path from 'path'
import ora from 'ora'
import slugify from 'slugify'
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 Project from '@/components/Project'
import ProjectNav from '@/components/ProjectNav'
@ -7,6 +5,8 @@ import { getAllSlugs } from '@/lib/getAllSlugs'
import { getProjectBySlug } from '@/lib/getProjectBySlug'
import meta from '@content/meta.json'
import projects from '@generated/projects.json'
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
type Props = {
params: { slug: string }
@ -21,10 +21,10 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
description: `${project.description.slice(0, 157)}...`,
metadataBase: new URL(meta.url),
alternates: {
canonical: '/' + project.slug
canonical: `/${project.slug}`
},
openGraph: {
url: '/' + project.slug,
url: `/${project.slug}`,
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 { render } from '@testing-library/react'
import projectMock from '../../../tests/__fixtures__/project.json'
import Page, { generateMetadata, generateStaticParams } from '../[slug]/page'
@ -31,10 +31,10 @@ describe('app: [slug]/page', () => {
description: `${projectMock.description.slice(0, 157)}...`,
metadataBase: new URL(meta.url),
alternates: {
canonical: '/' + projectMock.slug
canonical: `/${projectMock.slug}`
},
openGraph: {
url: '/' + projectMock.slug,
url: `/${projectMock.slug}`,
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>"
// https://github.com/testing-library/react-testing-library/issues/1250
let originalError: {
(...data: any[]): void
(message?: any, ...optionalParams: any[]): void
(...data: any[]): void
(message?: any, ...optionalParams: any[]): void
(...data: unknown[]): void
(message?: unknown, ...optionalParams: unknown[]): void
(...data: unknown[]): void
(message?: unknown, ...optionalParams: unknown[]): void
}
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 HostnameCheck from '@/components/HostnameCheck'
import ThemeSwitch from '@/components/ThemeSwitch'
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 from '@/styles/layout.module.css'
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.svg" type="image/svg+xml" />
<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 && (
<Script

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
'use client'
import { useEffect, useState, useTransition } from 'react'
import { getLocation } from '@/lib/getLocation'
import RelativeTime from '@yaireo/relative-time'
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 { Flag } from './Flag'
import styles from './Location.module.css'
import { UseLocation } from './types'
import type { UseLocation } from './types'
function Animation({ children }: { children: React.ReactNode }) {
const shouldReduceMotion = useReducedMotion()

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import type { ImageType } from '@/types'
import { render, screen } from '@testing-library/react'
import project from '@tests/__fixtures__/project.json'
import ProjectImage from '.'
import project from '../../../tests/__fixtures__/project.json'
describe('ProjectImage', () => {
it('renders correctly', async () => {
@ -17,7 +18,11 @@ describe('ProjectImage', () => {
it('returns without errors without image', async () => {
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 ImageType from '@/types/image'
import styles from './index.module.css'
export default function ProjectImage({

View File

@ -1,6 +1,6 @@
import { ForwardedRef, forwardRef } from 'react'
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 styles from './index.module.css'

View File

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

View File

@ -1,5 +1,5 @@
import type { ImageType } from '@/types'
import Link from 'next/link'
import ImageType from '../../types/image'
import ProjectImage from '../ProjectImage'
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 styles from './index.module.css'

View File

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

View File

@ -1,4 +1,4 @@
import Repo from '@/types/repo'
import type { Repo } from '@/types'
import Repository from '../Repository'
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>
<div className={styles.repos}>
{repos?.map((repo) => <Repository key={repo.name} repo={repo} />)}
{repos?.map((repo) => (
<Repository key={repo.name} repo={repo} />
))}
</div>
</>
)

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import saveAs from 'file-saver'
import avatar from '@/images/avatar.jpg'
import meta from '@content/meta.json'
import saveAs from 'file-saver'
import { imageToDataUrl } from './imageToDataUrl'
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'
const dummyPath = 'http://example.com/image.png'
@ -23,6 +23,7 @@ describe('imageToDataUrl', () => {
})
it('should convert image to data URL', async () => {
// biome-ignore lint/suspicious/noExplicitAny: not worth it for mocking
function MockFileReader(this: any) {
this.readAsDataURL = function () {
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)
@ -39,12 +40,12 @@ describe('imageToDataUrl', () => {
it('should handle errors in readAsDataURL', async () => {
function MockFileReader(this: FileReader) {
this.readAsDataURL = function () {
this.readAsDataURL = () => {
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
await expect(imageToDataUrl(dummyPath)).rejects.toThrow('Mock error')

View File

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

View File

@ -1,7 +1,7 @@
'use server'
import { revalidatePath } from 'next/cache'
import { GiphyFetch } from '@giphy/js-fetch-api'
import { revalidatePath } from 'next/cache'
export async function getRandomGif(tag: string, pathname?: string) {
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 fetch, { type FetchMock } from 'jest-fetch-mock'
import { getRepos } from './getRepos'
jest.mock('react', () => ({
...jest.requireActual('react'),
cache: (fn: any) => fn
cache: (fn: () => void) => fn
}))
describe('getRepos', () => {
@ -33,8 +32,9 @@ describe('getRepos', () => {
})
it('should handle network errors', async () => {
let consoleErrorMock: jest.SpyInstance
consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => {})
const consoleErrorMock: jest.SpyInstance = jest
.spyOn(console, 'error')
.mockImplementation(() => {})
;(fetch as FetchMock).mockRejectOnce(new Error('Network error'))
const data = await getRepos()

View File

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

View File

@ -1,9 +1,7 @@
declare type ImageType = {
export declare type ImageType = {
src: string
width?: number
height?: number
format?: 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[]
slug: string
title: string
description: string
descriptionHtml: 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
full_name: string
description: string
@ -7,5 +7,3 @@ declare type Repo = {
stargazers_count: number
pushed_at: string
}
export default Repo

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

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

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

@ -1,4 +1,5 @@
declare module '*.yml' {
// biome-ignore lint/suspicious/noExplicitAny: could be any data
const data: any
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>>(
(props, ref) => <svg ref={ref} {...props} />

View File

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