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:
parent
4eff043f17
commit
7eb0075e6f
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
19
.prettierrc
19
.prettierrc
@ -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
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-prettier/recommended"
|
||||
]
|
||||
}
|
12
README.md
12
README.md
@ -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
23
biome.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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
4208
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@ -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"]
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
10
public/sw.js
10
public/sw.js
@ -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))
|
||||
})
|
||||
})
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -1,4 +1,4 @@
|
||||
import ProjectType from '@/types/project'
|
||||
import type { ProjectType } from '@/types/project'
|
||||
import { getProjectImages } from './images'
|
||||
import { markdownToHtml } from './markdown'
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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 }]
|
||||
}
|
||||
}
|
||||
|
@ -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 }]
|
||||
}
|
||||
})
|
||||
|
@ -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(() => {
|
||||
|
@ -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
|
||||
|
@ -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.'
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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', () => {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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<{
|
||||
|
@ -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()
|
||||
|
@ -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 = {
|
||||
|
@ -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'
|
||||
|
@ -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}>
|
||||
|
@ -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"
|
||||
/>
|
||||
))}
|
||||
|
@ -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"
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -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({
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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', () => {
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
@ -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', () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Repo from '@/types/repo'
|
||||
import type { Repo } from '@/types'
|
||||
import Icon from '../Icon'
|
||||
import styles from './index.module.css'
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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')
|
||||
|
@ -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) => {
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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(
|
||||
|
@ -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
3
src/types/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './image'
|
||||
export * from './project'
|
||||
export * from './repo'
|
@ -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
|
||||
|
@ -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
26
src/types/umami.d.ts
vendored
@ -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
1
src/types/yaml.d.ts
vendored
@ -1,4 +1,5 @@
|
||||
declare module '*.yml' {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: could be any data
|
||||
const data: any
|
||||
export default data
|
||||
}
|
||||
|
@ -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} />
|
||||
|
@ -17,6 +17,7 @@
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@tests/*": ["./tests/*"],
|
||||
"@content/*": ["./_content/*"],
|
||||
"@generated/*": ["./generated/*"]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user