diff --git a/package-lock.json b/package-lock.json index 8850a07..9d30b83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -120,6 +120,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1941,6 +1942,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1964,6 +1966,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -3330,7 +3333,8 @@ "version": "10.7.0", "resolved": "https://registry.npmjs.org/@primer/primitives/-/primitives-10.7.0.tgz", "integrity": "sha512-732Dq7c4znkewwRkXldV0NLLppfGrDJHPXsXO2QLHwJxKLwi2icKkzcOR77vb5g0ThObZJrRqlB/4DYajIiyyQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@primer/react": { "version": "37.22.0", @@ -4170,6 +4174,7 @@ "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4511,6 +4516,7 @@ "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4524,6 +4530,7 @@ "version": "19.1.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4533,6 +4540,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz", "integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==", "devOptional": true, + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -4659,6 +4667,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.32.0", "@typescript-eslint/types": "8.32.0", @@ -5105,6 +5114,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5585,6 +5595,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", @@ -6731,6 +6742,7 @@ "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8654,6 +8666,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -9845,6 +9858,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -11795,6 +11809,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11803,6 +11818,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -11827,7 +11843,8 @@ "node_modules/react-is": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", - "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==" + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "peer": true }, "node_modules/react-markdown": { "version": "10.1.0", @@ -12160,6 +12177,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", "dev": true, + "peer": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -12924,6 +12942,7 @@ "version": "5.3.11", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", + "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.0.0", "@babel/traverse": "^7.4.5", @@ -13170,6 +13189,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -13405,6 +13425,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -13567,6 +13588,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13883,6 +13905,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -14040,6 +14063,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -14053,6 +14077,7 @@ "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "3.1.4", "@vitest/mocker": "3.1.4", @@ -14475,6 +14500,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14545,6 +14571,7 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, diff --git a/src/App.tsx b/src/App.tsx index 2720206..13ee36e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -111,13 +111,24 @@ function App() { // Sync state and actions from hooks into the zustand store so that // child components can subscribe to individual slices without a Provider. useEffect(() => { - useFormStore.setState({ + const nextState = { username, startDate, endDate, githubToken, apiMode, searchText, loading, loadingProgress, error, searchItemsCount, eventsCount, rawEventsCount, setUsername, setStartDate, setEndDate, setGithubToken, setApiMode, setSearchText, handleSearch, validateUsernameFormat, addAvatarsToCache, - }); + }; + const currentState = useFormStore.getState() as typeof nextState; + const changedState = (Object.keys(nextState) as (keyof typeof nextState)[]).reduce( + (acc, key) => { + if (currentState[key] !== nextState[key]) acc[key] = nextState[key] as never; + return acc; + }, + {} as Partial + ); + if (Object.keys(changedState).length > 0) { + useFormStore.setState(changedState); + } }, [ username, startDate, endDate, githubToken, apiMode, searchText, loading, loadingProgress, error, diff --git a/src/components/__tests__/Summary.test.tsx b/src/components/__tests__/Summary.test.tsx index d982a68..c12fdcd 100644 --- a/src/components/__tests__/Summary.test.tsx +++ b/src/components/__tests__/Summary.test.tsx @@ -11,13 +11,16 @@ const mockFormStore = { endDate: '2024-01-31', username: 'testuser', apiMode: 'summary' as const, + searchText: '', setUsername: vi.fn(), setStartDate: vi.fn(), setEndDate: vi.fn(), setGithubToken: vi.fn(), setApiMode: vi.fn(), + setSearchText: vi.fn(), handleSearch: vi.fn(), validateUsernameFormat: vi.fn(), + addAvatarsToCache: vi.fn(), loading: false, loadingProgress: '', error: null, diff --git a/src/store/useFormStore.ts b/src/store/useFormStore.ts index 0d13079..c4e9de4 100644 --- a/src/store/useFormStore.ts +++ b/src/store/useFormStore.ts @@ -29,7 +29,7 @@ interface FormStoreActions { setGithubToken: (token: string) => void; setApiMode: (mode: ApiMode) => void; setSearchText: (text: string) => void; - handleSearch: () => void; + handleSearch: () => Promise; validateUsernameFormat: (username: string) => void; addAvatarsToCache: (avatarUrls: { [username: string]: string }) => void; } @@ -39,9 +39,9 @@ type FormStore = FormStoreState & FormStoreActions; /** * Zustand store replacing the former React Context (FormContext). * - * State values and action callbacks are synced from App.tsx via - * `useFormStoreSync` so that the existing hooks remain the source of truth - * while consumers get fine-grained subscriptions without a Provider. + * State values and action callbacks are synced from App.tsx via a `useEffect` + * hook so that the existing hooks remain the source of truth while consumers + * get fine-grained subscriptions without a Provider. */ const useFormStore = create()(() => ({ // State defaults @@ -65,7 +65,7 @@ const useFormStore = create()(() => ({ setGithubToken: () => {}, setApiMode: () => {}, setSearchText: () => {}, - handleSearch: () => {}, + handleSearch: () => Promise.resolve(), validateUsernameFormat: () => {}, addAvatarsToCache: () => {}, }));