diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 949f6b22..7e39c183 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,10 +2,31 @@ name: CI on: pull_request: + types: [opened, synchronize, reopened, closed] branches: [main] +permissions: + contents: write + pull-requests: write + jobs: + lint: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - run: npm ci + + - run: npm run lint + test: + if: github.event.action != 'closed' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -17,8 +38,48 @@ jobs: - run: npm ci - - run: npm test + - run: npx vitest run --coverage - - run: npm run build + preview: + if: always() && (github.event.action == 'closed' || (needs.lint.result == 'success' && needs.test.result == 'success')) + needs: [lint, test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - if: github.event.action != 'closed' + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - if: github.event.action != 'closed' + run: npm ci + + - if: github.event.action != 'closed' + name: Build env: - NEXT_PUBLIC_BASE_PATH: /leetcode-patterns + NEXT_PUBLIC_BASE_PATH: /leetcode-patterns/pr-preview/pr-${{ github.event.number }} + run: npx next build + + - if: github.event.action != 'closed' + name: Remove service worker from preview + run: rm -f out/sw.js + + - name: Ensure .nojekyll exists at gh-pages root + run: | + git fetch origin gh-pages:gh-pages || true + if git rev-parse --verify gh-pages >/dev/null 2>&1; then + if ! git show gh-pages:.nojekyll >/dev/null 2>&1; then + git checkout gh-pages + touch .nojekyll + git add .nojekyll + git -c user.name="github-actions[bot]" -c user.email="github-actions[bot]@users.noreply.github.com" commit -m "Add .nojekyll for Next.js _next/ assets" + git push origin gh-pages + git checkout - + fi + fi + + - uses: rossjrw/pr-preview-action@v1 + with: + source-dir: out diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 41b69cea..b12f5b5c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,16 +12,10 @@ on: workflow_dispatch: permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: pages - cancel-in-progress: false + contents: write jobs: - build: + deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -36,18 +30,11 @@ jobs: - name: Build env: NEXT_PUBLIC_BASE_PATH: /leetcode-patterns - run: npx next build + run: npm run build - - uses: actions/upload-pages-artifact@v3 + - uses: JamesIves/github-pages-deploy-action@v4 with: - path: out - - deploy: - needs: build - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - id: deployment - uses: actions/deploy-pages@v4 + folder: out + branch: gh-pages + clean-exclude: pr-preview + force: false diff --git a/package-lock.json b/package-lock.json index dadf233b..ee8a8be5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^5.1.4", + "@vitest/coverage-v8": "^4.0.18", "eslint": "^9", "eslint-config-next": "16.1.6", "happy-dom": "^20.8.3", @@ -384,6 +385,15 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -3286,6 +3296,36 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", @@ -3643,6 +3683,23 @@ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -5321,6 +5378,12 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -5825,6 +5888,42 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -6333,6 +6432,44 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 8241a414..0b81c201 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^5.1.4", + "@vitest/coverage-v8": "^4.0.18", "eslint": "^9", "eslint-config-next": "16.1.6", "happy-dom": "^20.8.3", diff --git a/src/components/panels/AboutPanel.tsx b/src/components/panels/AboutPanel.tsx index d2bd9b4e..33d5bfa9 100644 --- a/src/components/panels/AboutPanel.tsx +++ b/src/components/panels/AboutPanel.tsx @@ -71,7 +71,7 @@ export default function AboutPanel() {
I believe everyone deserves - access to high-quality interview prep - regardless of their financial situation. It's why I chose to make this website{" "} + access to high-quality interview prep - regardless of their financial situation. It's why I chose to make this website{" "} free and open source.
diff --git a/src/components/questions/FilterToolbar.tsx b/src/components/questions/FilterToolbar.tsx
index ae31170c..9b8e57ea 100644
--- a/src/components/questions/FilterToolbar.tsx
+++ b/src/components/questions/FilterToolbar.tsx
@@ -1,4 +1,4 @@
-import { useState, useMemo, useEffect, useRef, useCallback } from "react";
+import { useState, useMemo, useEffect, useRef } from "react";
import { type Table } from "@tanstack/react-table";
import { Question } from "@/types/question";
import { RotateCcw, Shuffle, Download, Upload, Trash2, StarOff, Dices, ListOrdered } from "lucide-react";
@@ -63,6 +63,7 @@ export default function FilterToolbar({
}: FilterToolbarProps) {
const difficultyFilter = useMemo(
() => (table.getColumn("difficulty")?.getFilterValue() as string[]) ?? [],
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[table, columnFilters]
);
const [difficultyDropdownOpen, setDifficultyDropdownOpen] = useState(false);
@@ -70,6 +71,7 @@ export default function FilterToolbar({
const patternFilter = useMemo(
() => (table.getColumn("pattern")?.getFilterValue() as string[]) ?? [],
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[table, columnFilters]
);
const [patternDropdownOpen, setPatternDropdownOpen] = useState(false);
@@ -92,6 +94,7 @@ export default function FilterToolbar({
const companyFilter = useMemo(
() => (table.getColumn("companies")?.getFilterValue() as string[]) ?? [],
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[table, columnFilters]
);
const [companyDropdownOpen, setCompanyDropdownOpen] = useState(false);
diff --git a/src/components/questions/QuestionsTable.tsx b/src/components/questions/QuestionsTable.tsx
index 1a6da33a..e6114eea 100644
--- a/src/components/questions/QuestionsTable.tsx
+++ b/src/components/questions/QuestionsTable.tsx
@@ -457,7 +457,6 @@ export default function QuestionsTable({ data, updatedDate }: { data: Question[]
return vis;
}, [isMobile, columns]);
- // eslint-disable-next-line react-hooks/incompatible-library
const table = useReactTable({
data: filteredData,
columns,
@@ -784,6 +783,7 @@ export default function QuestionsTable({ data, updatedDate }: { data: Question[]
const tableBodyRef = useRef