1
0
mirror of https://github.com/kremalicious/portfolio.git synced 2024-12-22 17:23:22 +01:00
This commit is contained in:
Matthias Kretschmann 2024-02-06 01:06:58 +00:00
parent 0084295323
commit b7e03a1dbc
Signed by: m
GPG Key ID: 606EEEF3C479A91F
24 changed files with 218 additions and 101 deletions

68
package-lock.json generated
View File

@ -36,6 +36,7 @@
"eslint-config-next": "^14.1.0", "eslint-config-next": "^14.1.0",
"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",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"ora": "^8.0.1", "ora": "^8.0.1",
"prettier": "^3.2.4", "prettier": "^3.2.4",
@ -6599,6 +6600,15 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/cross-fetch": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
"integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==",
"dev": true,
"dependencies": {
"node-fetch": "^2.6.12"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -9918,6 +9928,16 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
} }
}, },
"node_modules/jest-fetch-mock": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz",
"integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==",
"dev": true,
"dependencies": {
"cross-fetch": "^3.0.4",
"promise-polyfill": "^8.1.3"
}
},
"node_modules/jest-get-type": { "node_modules/jest-get-type": {
"version": "29.6.3", "version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
@ -12037,6 +12057,48 @@
"tslib": "^2.0.3" "tslib": "^2.0.3"
} }
}, },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-fetch/node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
},
"node_modules/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
},
"node_modules/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/node-int64": { "node_modules/node-int64": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@ -12701,6 +12763,12 @@
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
} }
}, },
"node_modules/promise-polyfill": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
"integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==",
"dev": true
},
"node_modules/prompts": { "node_modules/prompts": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",

View File

@ -51,6 +51,7 @@
"eslint-config-next": "^14.1.0", "eslint-config-next": "^14.1.0",
"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",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"ora": "^8.0.1", "ora": "^8.0.1",
"prettier": "^3.2.4", "prettier": "^3.2.4",

View File

@ -1,13 +0,0 @@
'use server'
import { cache } from 'react'
import { getGithubRepos } from '@/lib/github'
export const getRepos = cache(async () => {
try {
const repos = await getGithubRepos()
return repos
} catch (error: unknown) {
console.error((error as Error).message)
}
})

View File

@ -1,3 +0,0 @@
export * from './getRandomGif'
export * from './getRepos'
export * from './getLocation'

View File

@ -3,10 +3,10 @@ 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'
import { getAllSlugs } from '@/lib/getAllSlugs'
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 { getAllSlugs } from './getAllSlugs'
import { getProjectBySlug } from './getProjectBySlug'
type Props = { type Props = {
params: { slug: string } params: { slug: string }

View File

@ -1,13 +1,13 @@
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import meta from '../../../_content/meta.json' import meta from '@content/meta.json'
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'
jest.mock('../[slug]/getProjectBySlug', () => ({ jest.mock('../../lib/getProjectBySlug', () => ({
getProjectBySlug: jest.fn().mockImplementation(() => projectMock) getProjectBySlug: jest.fn().mockImplementation(() => projectMock)
})) }))
jest.mock('../[slug]/getAllSlugs', () => ({ jest.mock('../../lib/getAllSlugs', () => ({
getAllSlugs: jest.fn().mockImplementationOnce(() => ['slug1', 'slug2']) getAllSlugs: jest.fn().mockImplementationOnce(() => ['slug1', 'slug2'])
})) }))

View File

@ -1,8 +1,9 @@
import { Suspense } from 'react' import { Suspense } from 'react'
import { getRepos, preloadLocation } from '@/actions'
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' import Repositories from '@/components/Repositories/Repositories'
import { preloadLocation } from '@/lib/getLocation'
import { getRepos } from '@/lib/getRepos'
import projects from '@generated/projects.json' import projects from '@generated/projects.json'
export default async function IndexPage() { export default async function IndexPage() {

View File

@ -3,7 +3,7 @@
import { MouseEvent, useEffect, useState } from 'react' import { MouseEvent, useEffect, useState } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import { getRandomGif } from '@/actions/getRandomGif' import { getRandomGif } from '@/lib/getRandomGif'
import Button from '../Button' import Button from '../Button'
import styles from './index.module.css' import styles from './index.module.css'

View File

@ -3,7 +3,7 @@
import { useEffect, useState, useTransition } from 'react' import { useEffect, useState, useTransition } from 'react'
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 '@/actions/getLocation' import { getLocation } from '@/lib/getLocation'
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'

View File

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

View File

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

View File

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

View File

@ -0,0 +1,51 @@
import fetch, { FetchMock } from 'jest-fetch-mock'
import { imageToDataUrl } from './imageToDataUrl'
const dummyPath = 'http://example.com/image.png'
const pixel = [
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49,
0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,
0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44,
0x41, 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0d,
0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42,
0x60, 0x82
]
describe('imageToDataUrl', () => {
beforeEach(() => {
fetch.resetMocks()
const mockBlob = new Blob([new Uint8Array(pixel)], { type: 'image/png' })
const mockResponse = new Response(mockBlob)
;(fetch as FetchMock).mockResponseOnce(async () => {
const text = await mockResponse.text()
return text
})
})
it('should convert image to data URL', async () => {
function MockFileReader() {
this.readAsDataURL = function () {
this.result = 'data:image/png;base64,...'
setTimeout(() => this.onload(), 0)
}
}
window.FileReader = MockFileReader as any
const dataUrl = await imageToDataUrl(dummyPath)
expect(dataUrl).toBe('data:image/png;base64,...')
})
it('should handle errors in readAsDataURL', async () => {
function MockFileReader() {
this.readAsDataURL = function () {
throw new Error('Mock error')
}
}
window.FileReader = MockFileReader as any
// Expect imageToDataUrl to reject with the mock error
await expect(imageToDataUrl(dummyPath)).rejects.toThrow('Mock error')
})
})

View File

@ -1,6 +1,7 @@
export async function imageToDataUrl(path: string): Promise<string> { export async function imageToDataUrl(path: string): Promise<string> {
const response = await fetch(path) const response = await fetch(path)
const blob = await response.blob() const blob = await response.blob()
return new Promise((onSuccess, onError) => { return new Promise((onSuccess, onError) => {
try { try {
const reader = new FileReader() const reader = new FileReader()

64
src/lib/getRepos.ts Normal file
View File

@ -0,0 +1,64 @@
'use server'
import { cache } from 'react'
import type Repo from '@/types/repo'
import filter from '@content/repos.json'
//
// Get GitHub repos
//
if (!process.env.GITHUB_TOKEN) {
throw new Error('Missing GitHub environment variable')
}
const gitHubConfig = {
headers: {
'User-Agent': 'kremalicious/portfolio',
Authorization: `token ${process.env.GITHUB_TOKEN}`
}
}
export const getRepos = cache(async () => {
try {
let repos: Repo[] = []
for (let item of filter) {
const user = item.split('/')[0]
const repoName = item.split('/')[1]
const response = await fetch(
`https://api.github.com/repos/${user}/${repoName}`,
gitHubConfig
)
const json: Repo = await response.json()
if (!json?.name) return
const {
name,
full_name,
description,
html_url,
homepage,
stargazers_count,
pushed_at
} = json
const repo: Repo = {
name,
full_name,
description,
html_url,
homepage,
stargazers_count,
pushed_at
}
repos.push(repo)
}
// sort by pushed to, newest first
repos = repos.sort((a, b) => b.pushed_at.localeCompare(a.pushed_at))
return repos
} catch (error: unknown) {
console.error((error as Error).message)
}
})

View File

@ -1,57 +0,0 @@
import type Repo from '@/types/repo'
import filter from '@content/repos.json'
//
// Get GitHub repos
//
if (!process.env.GITHUB_TOKEN) {
throw new Error('Missing GitHub environment variable')
}
const gitHubConfig = {
headers: {
'User-Agent': 'kremalicious/portfolio',
Authorization: `token ${process.env.GITHUB_TOKEN}`
}
}
export async function getGithubRepos() {
let repos: Repo[] = []
for (let item of filter) {
const user = item.split('/')[0]
const repoName = item.split('/')[1]
const data = await fetch(
`https://api.github.com/repos/${user}/${repoName}`,
gitHubConfig
)
const json: Repo = await data.json()
if (!json?.name) return
const {
name,
full_name,
description,
html_url,
homepage,
stargazers_count,
pushed_at
} = json
const repo: Repo = {
name,
full_name,
description,
html_url,
homepage,
stargazers_count,
pushed_at
}
repos.push(repo)
}
// sort by pushed to, newest first
repos = repos.sort((a, b) => b.pushed_at.localeCompare(a.pushed_at))
return repos
}

View File

@ -7,7 +7,7 @@ const createJestConfig = nextJest({
/** @type {import('jest').Config} */ /** @type {import('jest').Config} */
const customJestConfig = { const customJestConfig = {
rootDir: '../', rootDir: '../',
setupFilesAfterEnv: ['<rootDir>/tests/jest.setup.tsx'], setupFilesAfterEnv: ['<rootDir>/tests/jest.setup.ts'],
moduleDirectories: ['node_modules', '<rootDir>/src'], moduleDirectories: ['node_modules', '<rootDir>/src'],
testEnvironment: 'jsdom', testEnvironment: 'jsdom',
moduleNameMapper: { moduleNameMapper: {

View File

@ -1,26 +1,29 @@
import { jest } from '@jest/globals' import { jest } from '@jest/globals'
import '@testing-library/jest-dom' import '@testing-library/jest-dom'
import fetchMock from 'jest-fetch-mock'
import giphyMock from './__fixtures__/giphy.json' import giphyMock from './__fixtures__/giphy.json'
import { dataLocation } from './__fixtures__/location' import { dataLocation } from './__fixtures__/location'
import reposMock from './__fixtures__/repos.json' import reposMock from './__fixtures__/repos.json'
import './__mocks__/matchMedia' import './__mocks__/matchMedia'
fetchMock.enableMocks()
jest.mock('next/navigation', () => ({ jest.mock('next/navigation', () => ({
usePathname: jest.fn().mockImplementationOnce(() => '/') usePathname: jest.fn().mockImplementationOnce(() => '/')
})) }))
jest.mock('../src/actions/getLocation', () => ({ jest.mock('../src/lib/getLocation', () => ({
getLocation: jest.fn().mockImplementation(() => dataLocation), getLocation: jest.fn().mockImplementation(() => dataLocation),
preloadLocation: jest.fn() preloadLocation: jest.fn()
})) }))
jest.mock('../src/actions/getRandomGif', () => ({ jest.mock('../src/lib/getRandomGif', () => ({
getRandomGif: jest getRandomGif: jest
.fn() .fn()
.mockImplementation(() => giphyMock.data.images.original.mp4) .mockImplementation(() => giphyMock.data.images.original.mp4)
})) }))
jest.mock('../src/actions/getRepos', () => ({ jest.mock('../src/lib/getRepos', () => ({
getRepos: jest.fn().mockImplementation(() => reposMock) getRepos: jest.fn().mockImplementation(() => reposMock)
})) }))