diff --git a/eslint.config.mjs b/eslint.config.mjs
index 247b5e5c4..12b67ed2b 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,20 +1,68 @@
// @ts-check
+import { readdirSync } from 'fs';
+import { dirname, join } from 'path';
+import { fileURLToPath } from 'url';
+
import eslint from '@eslint/js';
+import perfectionist from 'eslint-plugin-perfectionist';
import tseslint from 'typescript-eslint';
+const _filename = fileURLToPath(import.meta.url);
+const _dirname = dirname(_filename);
+const baseDir = join(_dirname, 'src');
+const internalDirectories = readdirSync(baseDir, {
+ withFileTypes: true,
+}).flatMap((dirent) => (dirent.isDirectory() ? dirent.name : []));
+
export default tseslint.config(
eslint.configs.recommended,
- tseslint.configs.strict,
- tseslint.configs.stylistic,
- { ignores: ['build', 'deprecated'] },
+ tseslint.configs.strictTypeChecked,
+ tseslint.configs.stylisticTypeChecked,
+ perfectionist.configs['recommended-natural'],
+ { ignores: ['build', 'coverage', 'deprecated'] },
{
+ languageOptions: {
+ parserOptions: {
+ projectService: true,
+ tsconfigRootDir: _dirname,
+ },
+ },
rules: {
eqeqeq: 'error',
- '@typescript-eslint/no-empty-function': [
+ '@typescript-eslint/no-unnecessary-condition': 'off',
+ 'perfectionist/sort-imports': [
'error',
- { allow: ['arrowFunctions'] },
+ {
+ customGroups: {
+ value: {
+ react: ['^react$', '^react-.+'],
+ },
+ },
+ groups: [
+ 'react',
+ 'type',
+ 'builtin',
+ 'external',
+ 'internal-type',
+ 'internal',
+ ['parent-type', 'sibling-type', 'index-type'],
+ ['parent', 'sibling', 'index'],
+ 'object',
+ 'unknown',
+ ],
+ internalPattern: [`^(${internalDirectories.join('|')})(/|$)`],
+ sortSideEffects: true,
+ },
],
+ 'perfectionist/sort-interfaces': 'off',
+ 'perfectionist/sort-jsx-props': 'off',
+ 'perfectionist/sort-modules': 'off',
+ 'perfectionist/sort-objects': 'off',
},
},
+ {
+ files: ['**/*.mjs'],
+ extends: [tseslint.configs.disableTypeChecked],
+ },
);
diff --git a/package-lock.json b/package-lock.json
index 37b2fadbe..1dea4bad9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"eslint": "^9.16.0",
+ "eslint-plugin-perfectionist": "^4.2.0",
"husky": "^9.1.7",
"jest-canvas-mock": "^2.5.2",
"lint-staged": "^15.2.10",
@@ -8472,6 +8473,24 @@
"node": ">= 0.4"
}
},
+ "node_modules/eslint-plugin-perfectionist": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-4.2.0.tgz",
+ "integrity": "sha512-hEMFx5xfSc/0OLZXJhSaLUKkFxATuRf4yL2iVfxEcxkkb17DfoLZY9eH960dPSw5uB7o+4avqP3rtkNp1Vwo7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "^8.17.0",
+ "@typescript-eslint/utils": "^8.17.0",
+ "natural-orderby": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=8.0.0"
+ }
+ },
"node_modules/eslint-plugin-react": {
"version": "7.37.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz",
@@ -8806,10 +8825,11 @@
}
},
"node_modules/express": {
- "version": "4.21.1",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
- "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -8830,7 +8850,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
+ "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -8845,6 +8865,10 @@
},
"engines": {
"node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/array-flatten": {
@@ -12973,6 +12997,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/natural-orderby": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-5.0.0.tgz",
+ "integrity": "sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -13465,10 +13499,11 @@
"dev": true
},
"node_modules/path-to-regexp": {
- "version": "0.1.10",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
- "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
- "dev": true
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/path-type": {
"version": "4.0.0",
diff --git a/package.json b/package.json
index bc9eda3f0..4c4d655de 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"eslint": "^9.16.0",
+ "eslint-plugin-perfectionist": "^4.2.0",
"husky": "^9.1.7",
"jest-canvas-mock": "^2.5.2",
"lint-staged": "^15.2.10",
diff --git a/src/App/App.tsx b/src/App/App.tsx
index 0d6c11431..018ca9ddc 100644
--- a/src/App/App.tsx
+++ b/src/App/App.tsx
@@ -1,8 +1,9 @@
import { useEffect, useState } from 'react';
+import { Buttons, Content, Footer, Particles, Toggle } from 'components';
+
import './App.scss';
import { AppProvider } from './AppContext';
-import { Buttons, Content, Footer, Particles, Toggle } from 'components';
import { config } from './config';
export const App = () => {
@@ -11,9 +12,9 @@ export const App = () => {
const init = () => {
if (
- window.matchMedia(
+ matchMedia(
'(max-device-width: 820px) and (-webkit-min-device-pixel-ratio: 2)',
- )?.matches
+ ).matches
) {
setIsMobile(true);
}
diff --git a/src/App/AppContext.tsx b/src/App/AppContext.tsx
index 523986dc0..7c18ae06e 100644
--- a/src/App/AppContext.tsx
+++ b/src/App/AppContext.tsx
@@ -16,7 +16,7 @@ interface AppContextInterface extends AppProviderInterface {
const actions = { SET_THEME: 'SET_THEME' } as const;
interface AppAction {
- type: 'SET_THEME';
+ type: typeof actions.SET_THEME;
value: string;
}
@@ -26,7 +26,7 @@ const initialState: AppState = {
config: {} as Config,
isMobile: false,
theme: themes.dark,
- setTheme: () => {},
+ setTheme: () => undefined,
};
export const reducer = (state: AppState, action: AppAction): AppState => {
@@ -58,7 +58,9 @@ export const AppProvider = ({
config: state.config,
isMobile: state.isMobile,
theme: state.theme,
- setTheme: (value) => dispatch({ type: actions.SET_THEME, value }),
+ setTheme: (value) => {
+ dispatch({ type: actions.SET_THEME, value });
+ },
};
return {children};
diff --git a/src/App/config.tsx b/src/App/config.tsx
index 7f5c6e218..215fbc0fe 100644
--- a/src/App/config.tsx
+++ b/src/App/config.tsx
@@ -1,5 +1,5 @@
-import { Config } from 'types';
import { Email, GitHub, LinkedIn, Resume } from 'icons';
+import { Config } from 'types';
export const config: Config = {
name: {
diff --git a/src/Index.test.tsx b/src/Index.test.tsx
index 232be7414..38367b915 100644
--- a/src/Index.test.tsx
+++ b/src/Index.test.tsx
@@ -1,11 +1,12 @@
-import { configure, fireEvent, render, screen } from '@testing-library/react';
import { act } from 'react';
+import { configure, fireEvent, render, screen } from '@testing-library/react';
+
import '__mocks__/matchMedia';
import { App } from 'App/App';
import { AppProvider, reducer } from 'App/AppContext';
-import { Footer } from 'components';
import { themes } from 'appearance';
+import { Footer } from 'components';
configure({ testIdAttribute: 'data-v2' });
@@ -25,12 +26,12 @@ const mockState = {
},
isMobile: false,
theme: themes.dark,
- setTheme: () => {},
+ setTheme: () => undefined,
};
describe('application tests', () => {
beforeEach(async () => {
- await act(async () => render());
+ await act(() => render());
});
/**
@@ -166,7 +167,7 @@ describe('application tests', () => {
describe('app context tests', () => {
it('should render partial footer on mobile', async () => {
- await act(async () =>
+ await act(() =>
render(
{
it("should show the dark theme when 'theme' is set to 'true' in local storage", async () => {
// set local storage item and render the app
localStorage.setItem('theme', 'true');
- await act(async () => render());
+ await act(() => render());
// check that the local storage item has been updated correctly
expect(localStorage.getItem('theme')).toEqual('dark');
@@ -217,7 +218,7 @@ describe('local storage tests', () => {
it("should show the light theme when 'theme' is set to 'false' in local storage", async () => {
// set local storage item and render the app
localStorage.setItem('theme', 'false');
- await act(async () => render());
+ await act(() => render());
// check that the local storage item has been updated correctly
expect(localStorage.getItem('theme')).toEqual('light');
@@ -228,13 +229,15 @@ describe('local storage tests', () => {
// https://testing-library.com/docs/react-testing-library/api/#rerender
it('should persist the light theme through an app re-render', async () => {
- const { rerender } = render();
+ const { rerender } = await act(() => render());
expect(localStorage.getItem('theme')).toBeNull();
localStorage.setItem('theme', 'light');
// re-render the app and check the theme
- await act(async () => rerender());
+ act(() => {
+ rerender();
+ });
const particles = screen.getByTestId('particles');
expect(localStorage.getItem('theme')).toEqual('light');
@@ -244,7 +247,7 @@ describe('local storage tests', () => {
it('should change local storage value when toggle is clicked', async () => {
// set local storage item and render the app
localStorage.setItem('theme', 'light');
- await act(async () => render());
+ await act(() => render());
// click the toggle
const toggle = screen.getByTestId('toggle');
diff --git a/src/__mocks__/matchMedia.ts b/src/__mocks__/matchMedia.ts
index 099fa5177..edc818e60 100644
--- a/src/__mocks__/matchMedia.ts
+++ b/src/__mocks__/matchMedia.ts
@@ -1,6 +1,6 @@
Object.defineProperty(window, 'matchMedia', {
writable: true,
- value: jest.fn().mockImplementation((query) => ({
+ value: (query: string) => ({
matches: false,
media: query,
onchange: null,
@@ -9,5 +9,5 @@ Object.defineProperty(window, 'matchMedia', {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
- })),
+ }),
});
diff --git a/src/components/Buttons.tsx b/src/components/Buttons.tsx
index 8f1265205..1c409dd06 100644
--- a/src/components/Buttons.tsx
+++ b/src/components/Buttons.tsx
@@ -1,4 +1,5 @@
import { useContext } from 'react';
+
import styled from 'styled-components';
import { AppContext } from 'App/AppContext';
diff --git a/src/components/Content.tsx b/src/components/Content.tsx
index a3e521907..2c298ded8 100644
--- a/src/components/Content.tsx
+++ b/src/components/Content.tsx
@@ -1,4 +1,5 @@
import { useContext } from 'react';
+
import styled, { css } from 'styled-components';
import { AppContext } from 'App/AppContext';
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
index f1d00ff4b..a0927dc59 100644
--- a/src/components/Footer.tsx
+++ b/src/components/Footer.tsx
@@ -1,4 +1,5 @@
import { useContext } from 'react';
+
import styled from 'styled-components';
import { AppContext } from 'App/AppContext';
diff --git a/src/components/Particles.tsx b/src/components/Particles.tsx
index 8b08a5894..fb0fc1e9b 100644
--- a/src/components/Particles.tsx
+++ b/src/components/Particles.tsx
@@ -1,7 +1,8 @@
import { useContext } from 'react';
-import styled from 'styled-components';
import ReactParticles from 'react-tsparticles';
+import styled from 'styled-components';
+
import { AppContext } from 'App/AppContext';
import { options } from 'appearance';
import { Theme } from 'types';
diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx
index e09ec26f1..e41d214b6 100644
--- a/src/components/Toggle.tsx
+++ b/src/components/Toggle.tsx
@@ -1,4 +1,5 @@
import { useContext } from 'react';
+
import styled from 'styled-components';
import { AppContext } from 'App/AppContext';
diff --git a/src/index.tsx b/src/index.tsx
index bef7c1efe..4c684c1b5 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,5 +1,5 @@
-import { createRoot } from 'react-dom/client';
import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
import { App } from 'App/App';
diff --git a/src/setupTests.ts b/src/setupTests.ts
index a39c871f0..5e62117da 100644
--- a/src/setupTests.ts
+++ b/src/setupTests.ts
@@ -5,6 +5,6 @@
import '@testing-library/jest-dom';
import 'jest-canvas-mock';
-console.error = (message) => {
+console.error = (message: string) => {
throw new Error(`Console error: ${message}`);
};
diff --git a/src/types/config.interface.ts b/src/types/config.interface.ts
index e62a1072c..23511c52f 100644
--- a/src/types/config.interface.ts
+++ b/src/types/config.interface.ts
@@ -1,3 +1,5 @@
+import type { JSX } from 'react';
+
export interface Content {
display: string;
}