diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..8cfc622 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,32 @@ +name: Lint and Type Check + +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run lint and type check + run: npm run check diff --git a/README.md b/README.md index 223d566..e088cef 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ # Nova +[![Release](https://img.shields.io/github/v/release/algotyrnt/nova?display_name=tag)](https://github.com/algotyrnt/nova/releases) +[![CodeQL](https://github.com/algotyrnt/nova/actions/workflows/codeql.yml/badge.svg)](https://github.com/algotyrnt/nova/actions/workflows/codeql.yml) +[![Lint and Type Check](https://github.com/algotyrnt/nova/actions/workflows/check.yml/badge.svg)](https://github.com/algotyrnt/nova/actions/workflows/check.yml) +[![License: MIT](https://img.shields.io/github/license/algotyrnt/nova)](https://github.com/algotyrnt/nova/blob/main/LICENSE) + A minimal, fast, and fully customizable personal portfolio site built with `Next.js` (App Router), `TypeScript`, `MUI`, and `Framer Motion`. It can render your pinned GitHub repositories and latest Medium posts at build/runtime with ISR. Live site: [algotyrnt.com](https://algotyrnt.com) - ## Features - App Router + React Server Components @@ -154,7 +158,3 @@ Set the same environment variables from `.env.example` in the Vercel project set npm run build npm run start ``` - -## License - -MIT diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 7f95a91..0000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -import prettier from 'eslint-plugin-prettier/recommended' - -export default [ - { - ignores: ['.next/', 'node_modules/'], - }, - prettier, -] diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 0000000..1ff86de --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,15 @@ +import type { Linter } from 'eslint' +import nextCoreWebVitals from 'eslint-config-next/core-web-vitals' +import nextTypescript from 'eslint-config-next/typescript' +import prettier from 'eslint-plugin-prettier/recommended' + +const config: Linter.Config[] = [ + ...nextCoreWebVitals, + ...nextTypescript, + { + ignores: ['.next/', 'node_modules/'], + }, + prettier, +] + +export default config diff --git a/package-lock.json b/package-lock.json index 7b35b98..3e44efd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nova", - "version": "2.0.7", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nova", - "version": "2.0.7", + "version": "2.1.0", "license": "MIT", "dependencies": { "@emotion/react": "^11.14.0", @@ -30,6 +30,7 @@ "eslint-config-next": "^16.1.6", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", + "jiti": "^2.6.1", "prettier": "^3.7.4", "typescript": "^5" } @@ -4811,6 +4812,16 @@ "node": ">= 0.4" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index cfd37c6..ef31fa5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nova", - "version": "2.0.7", + "version": "2.1.0", "private": true, "license": "MIT", "author": { @@ -16,7 +16,9 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint ." + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "check": "npm run lint && npm run typecheck" }, "dependencies": { "@emotion/react": "^11.14.0", @@ -40,6 +42,7 @@ "eslint-config-next": "^16.1.6", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", + "jiti": "^2.6.1", "prettier": "^3.7.4", "typescript": "^5" } diff --git a/src/app/error.tsx b/src/app/error.tsx index 3c86b0c..e0ddc4b 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -18,10 +18,25 @@ export default function Error({ }, [error]) return ( - - Something went wrong! - An unexpected error occurred while loading this page. - + + + Something went wrong! + + + An unexpected error occurred while loading this page. + + ) } diff --git a/src/components/ThemeRegistry/EmotionCache.tsx b/src/components/ThemeRegistry/EmotionCache.tsx index 0aabb20..a555bc4 100644 --- a/src/components/ThemeRegistry/EmotionCache.tsx +++ b/src/components/ThemeRegistry/EmotionCache.tsx @@ -1,55 +1,55 @@ -'use client'; +'use client' // Adapted from https://mui.com/material-ui/guides/nextjs/ -import * as React from 'react'; -import { CacheProvider } from '@emotion/react'; -import createCache, { type Options as CacheOptions } from '@emotion/cache'; -import { useServerInsertedHTML } from 'next/navigation'; +import * as React from 'react' +import { CacheProvider } from '@emotion/react' +import createCache, { type Options as CacheOptions } from '@emotion/cache' +import { useServerInsertedHTML } from 'next/navigation' export default function NextAppDirEmotionCacheProvider({ - options, - children, + options, + children, }: { - options: CacheOptions; - children: React.ReactNode; + options: CacheOptions + children: React.ReactNode }) { - const [{ cache, flush }] = React.useState(() => { - const cache = createCache(options); - cache.compat = true; - const prevInsert = cache.insert; - let inserted: string[] = []; - cache.insert = (...args) => { - const serialized = args[1]; - if (cache.inserted[serialized.name] === undefined) { - inserted.push(serialized.name); - } - return prevInsert(...args); - }; - const flush = () => { - const prevInserted = inserted; - inserted = []; - return prevInserted; - }; - return { cache, flush }; - }); + const [{ cache, flush }] = React.useState(() => { + const cache = createCache(options) + cache.compat = true + const prevInsert = cache.insert + let inserted: string[] = [] + cache.insert = (...args) => { + const serialized = args[1] + if (cache.inserted[serialized.name] === undefined) { + inserted.push(serialized.name) + } + return prevInsert(...args) + } + const flush = () => { + const prevInserted = inserted + inserted = [] + return prevInserted + } + return { cache, flush } + }) - useServerInsertedHTML(() => { - const names = flush(); - if (names.length === 0) { - return null; - } - let styles = ''; - for (const name of names) { - styles += cache.inserted[name]; - } - return ( -