From 4f516573ab0511a5b91cd130b648fa9420cc88ce Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 6 Nov 2023 17:21:59 +0200 Subject: [PATCH 01/69] refactor: rename tab component --- src/features/MovieList/MovieList.tsx | 12 ++++++------ src/shared/ui/{Select.tsx => Tabs.tsx} | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) rename src/shared/ui/{Select.tsx => Tabs.tsx} (92%) diff --git a/src/features/MovieList/MovieList.tsx b/src/features/MovieList/MovieList.tsx index 99e9e68..7e85d42 100644 --- a/src/features/MovieList/MovieList.tsx +++ b/src/features/MovieList/MovieList.tsx @@ -8,7 +8,7 @@ import PageNum from './ui/PageNum.tsx'; import Movie from '../../entities/movie/ui/Movie.tsx'; import { ItemsPerPage } from '../../shared/types/types.ts'; import Loader from '../../shared/ui/Loader.tsx'; -import Select from '../../shared/ui/Select.tsx'; +import Tabs from '../../shared/ui/Tabs.tsx'; import Tooltip from '../../shared/ui/Tooltip.tsx'; interface IMovieListProps { @@ -46,11 +46,11 @@ function MovieList({ scroll }: IMovieListProps) { data-scroll-sticky="true" data-scroll-target="section" className="absolute -top-24 z-10 flex items-center justify-center gap-4"> - handler={setMoviesPerPage} value={moviesPerPage}> - value={3}>3 movies - value={5}>5 movies - value={10}>10 movies - + handler={setMoviesPerPage} value={moviesPerPage}> + value={3}>3 movies + value={5}>5 movies + value={10}>10 movies + {renderMovies?.map((movie, i) => ( diff --git a/src/shared/ui/Select.tsx b/src/shared/ui/Tabs.tsx similarity index 92% rename from src/shared/ui/Select.tsx rename to src/shared/ui/Tabs.tsx index d77e3fb..c001169 100644 --- a/src/shared/ui/Select.tsx +++ b/src/shared/ui/Tabs.tsx @@ -26,7 +26,7 @@ interface ISelectContext { const SelectContext = createContext(null); -function Select({ +function Tabs({ children, handler, value, @@ -54,7 +54,7 @@ function Select({ ); } -function Option({ +function Tab({ children, value, }: IOptionProps) { @@ -77,6 +77,6 @@ function Option({ ); } -Select.Option = Option; +Tabs.Tab = Tab; -export default Select; +export default Tabs; From c56a34e93354980d1a1ec716ef8d07aac30f352c Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 7 Nov 2023 20:28:57 +0200 Subject: [PATCH 02/69] fix: locomotive scroll causing error in test environment --- .../{ => AppLayout}/ui/GradientBackground.tsx | 0 src/shared/hooks/useScroll.ts | 25 +++++++++++-------- src/shared/hooks/useScrollTop.ts | 2 +- src/shared/hooks/useTooltip.ts | 2 +- 4 files changed, 17 insertions(+), 12 deletions(-) rename src/pages/{ => AppLayout}/ui/GradientBackground.tsx (100%) diff --git a/src/pages/ui/GradientBackground.tsx b/src/pages/AppLayout/ui/GradientBackground.tsx similarity index 100% rename from src/pages/ui/GradientBackground.tsx rename to src/pages/AppLayout/ui/GradientBackground.tsx diff --git a/src/shared/hooks/useScroll.ts b/src/shared/hooks/useScroll.ts index a17c2c8..2f70d5a 100644 --- a/src/shared/hooks/useScroll.ts +++ b/src/shared/hooks/useScroll.ts @@ -8,19 +8,24 @@ function useScroll() { const observerRef = useRef(); useEffect(() => { - if (containerRef.current) { + if (containerRef.current && LocomotiveScroll) { containerRef.current.setAttribute('data-Scroll-container', 'true'); - scrollRef.current = new LocomotiveScroll({ - el: containerRef.current, - smooth: true, - smartphone: { + try { + scrollRef.current = new LocomotiveScroll({ + el: containerRef.current, smooth: true, - }, - touchMultiplier: 6, - lerp: 0.2, - multiplier: 1.6, - }); + smartphone: { + smooth: true, + }, + touchMultiplier: 6, + lerp: 0.2, + multiplier: 1.6, + }); + } catch (e) { + // if the error is caught that means the test environment is used + observerRef.current?.disconnect?.(); + } observerRef.current = new ResizeObserver( () => scrollRef.current?.update(), diff --git a/src/shared/hooks/useScrollTop.ts b/src/shared/hooks/useScrollTop.ts index 169bff1..b7476cb 100644 --- a/src/shared/hooks/useScrollTop.ts +++ b/src/shared/hooks/useScrollTop.ts @@ -16,7 +16,7 @@ function useScrollTop( useEffect(() => { if (prevValueRef.current !== currValue) { prevValueRef.current = currValue; - scroll?.current?.scrollTo('top', { duration: SCROLL_TOP_DURATION }); + scroll?.current?.scrollTo?.('top', { duration: SCROLL_TOP_DURATION }); callback?.(); } }, [currValue, scroll, ...deps]); diff --git a/src/shared/hooks/useTooltip.ts b/src/shared/hooks/useTooltip.ts index dc6efc3..4590f5f 100644 --- a/src/shared/hooks/useTooltip.ts +++ b/src/shared/hooks/useTooltip.ts @@ -31,7 +31,7 @@ function useTooltip(scroll: RefObject) { useEffect(() => { if (scroll.current) - scroll.current.on('scroll', () => { + scroll.current.on?.('scroll', () => { if (tooltipRef.current) tooltipRef.current.classList.add(...HIDDEN); }); }, [scroll]); From 7c2c31756abc4e4c7709757c19fb14e26de1be0d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 7 Nov 2023 20:59:32 +0200 Subject: [PATCH 03/69] refactor: rearrange files --- src/app/router.tsx | 8 +++++--- src/pages/{ => AppLayout}/AppLayout.tsx | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 14 deletions(-) rename src/pages/{ => AppLayout}/AppLayout.tsx (60%) diff --git a/src/app/router.tsx b/src/app/router.tsx index 2ce07b4..48a4142 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -1,10 +1,10 @@ import { createHashRouter } from 'react-router-dom'; import loader from '../entities/movie/loader.ts'; -import AppLayout from '../pages/AppLayout.tsx'; +import AppLayout from '../pages/AppLayout/AppLayout.tsx'; import MovieDetails from '../widgets/MovieDetails/MovieDetails.tsx'; -const router = createHashRouter([ +export const ROUTES = [ { element: , path: '/', @@ -16,6 +16,8 @@ const router = createHashRouter([ }, ], }, -]); +]; + +const router = createHashRouter(ROUTES); export default router; diff --git a/src/pages/AppLayout.tsx b/src/pages/AppLayout/AppLayout.tsx similarity index 60% rename from src/pages/AppLayout.tsx rename to src/pages/AppLayout/AppLayout.tsx index 622e671..c6c7249 100644 --- a/src/pages/AppLayout.tsx +++ b/src/pages/AppLayout/AppLayout.tsx @@ -1,17 +1,17 @@ import { Outlet, useNavigation } from 'react-router-dom'; import GradientBackground from './ui/GradientBackground.tsx'; -import MovieList from '../features/MovieList/MovieList.tsx'; -import Pagination from '../features/Pagination/Pagination.tsx'; -import SearchProvider from '../features/Search/context/SearchProvider.tsx'; -import Search from '../features/Search/Search.tsx'; -import { LOADING_STATE } from '../shared/const/const.ts'; -import useScroll from '../shared/hooks/useScroll.ts'; -import Loader from '../shared/ui/Loader.tsx'; -import Header from '../widgets/Header/Header.tsx'; -import Logo from '../widgets/Header/ui/Logo.tsx'; -import TotalResults from '../widgets/Header/ui/TotalResults.tsx'; -import Main from '../widgets/Main/Main.tsx'; +import MovieList from '../../features/MovieList/MovieList.tsx'; +import Pagination from '../../features/Pagination/Pagination.tsx'; +import SearchProvider from '../../features/Search/context/SearchProvider.tsx'; +import Search from '../../features/Search/Search.tsx'; +import { LOADING_STATE } from '../../shared/const/const.ts'; +import useScroll from '../../shared/hooks/useScroll.ts'; +import Loader from '../../shared/ui/Loader.tsx'; +import Header from '../../widgets/Header/Header.tsx'; +import Logo from '../../widgets/Header/ui/Logo.tsx'; +import TotalResults from '../../widgets/Header/ui/TotalResults.tsx'; +import Main from '../../widgets/Main/Main.tsx'; function AppLayout() { const { containerRef, scrollRef } = useScroll(); From 056856f5672f4d3150bc361bc5386a9d4d5f0df0 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 7 Nov 2023 21:00:29 +0200 Subject: [PATCH 04/69] chore: add vitest & react testing library --- package-lock.json | 1724 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 14 +- 2 files changed, 1731 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 14a9d3a..5ec4bf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "react-ts-tw-template", + "name": "cinemania", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "react-ts-tw-template", + "name": "cinemania", "version": "0.0.0", "dependencies": { "locomotive-scroll": "^4.1.4", @@ -14,12 +14,18 @@ "react-router-dom": "^6.18.0" }, "devDependencies": { + "@edge-runtime/vm": "^3.1.7", + "@testing-library/dom": "^9.3.3", + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.5.1", "@types/locomotive-scroll": "^4.1.2", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.0.3", + "@vitest/coverage-v8": "^0.34.6", "autoprefixer": "^10.4.16", "eslint": "^8.50.0", "eslint-config-airbnb": "^19.0.4", @@ -41,7 +47,8 @@ "prettier-plugin-tailwindcss": "^0.5.4", "tailwindcss": "^3.3.3", "typescript": "^5.2.2", - "vite": "^4.4.9" + "vite": "^4.4.9", + "vitest": "^0.34.6" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -53,6 +60,12 @@ "node": ">=0.10.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz", + "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==", + "dev": true + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -435,6 +448,33 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@edge-runtime/primitives": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@edge-runtime/primitives/-/primitives-4.0.5.tgz", + "integrity": "sha512-t7QiN5d/KpXgCvIfSt6Nm9Hj3WVdNgc5CpOD73jasY+9EvTI7Ngdj5cXvjcHrPcmYWJZMySPgeEeoL/1N/Llag==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/vm": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@edge-runtime/vm/-/vm-3.1.7.tgz", + "integrity": "sha512-hUMFbDQ/nZN+1TLMi6iMO1QFz9RSV8yGG8S42WFPFma1d7VSNE0eMdJUmwjmtav22/iQkzHMmu6oTSfAvRGS8g==", + "dev": true, + "dependencies": { + "@edge-runtime/primitives": "4.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -903,6 +943,27 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -1014,6 +1075,298 @@ "node": ">=14.0.0" } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@testing-library/dom": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", + "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/@testing-library/dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.1.4.tgz", + "integrity": "sha512-wpoYrCYwSZ5/AxcrjLxJmCU6I5QAJXslEeSiMQqaWmP2Kzpd1LvF/qxmAIW2qposULGWq2gw30GgVNFLSc2Jnw==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.3.1", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/jest-dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", + "integrity": "sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.5.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.1.tgz", + "integrity": "sha512-UCcUKrUYGj7ClomOo2SpNVvx4/fkd/2BbIHDCle8A0ax+P3bU7yJwDBDrS6ZwdTMARWTGODX1hEsCcO+7beJjg==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.3.tgz", + "integrity": "sha512-0Z6Tr7wjKJIk4OUEjVUQMtyunLDy339vcMaj38Kpj6jM2OE1p3S4kXExKZ7a3uXQAPCoy3sbrP1wibDKaf39oA==", + "dev": true + }, "node_modules/@types/babel__core": { "version": "7.20.3", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.3.tgz", @@ -1055,6 +1408,27 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/chai": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.9.tgz", + "integrity": "sha512-69TtiDzu0bcmKQv3yg1Zx409/Kd7r0b5F1PfpYJfSHzLGtB53547V4u+9iqKYsTu/O2ai6KTb0TInNpvuQ3qmg==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.4.tgz", + "integrity": "sha512-CCWNXrJYSUIojZ1149ksLl3AN9cmZ5djf+yUoVVV+NuYrtydItQVlL2ZDqyC6M6O9LWRnVf8yYDxbXHO2TfQZg==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.14", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", @@ -1073,6 +1447,15 @@ "integrity": "sha512-K4/YWVuLf+xW5lXPue8RdWAm96dVPlyn8ISqxGdK9QflLFy82cDsvpyHJchcKtfp+qNV9OZ11nq56T5oa8jogA==", "dev": true }, + "node_modules/@types/node": { + "version": "20.8.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", + "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/prop-types": { "version": "15.7.9", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", @@ -1325,6 +1708,134 @@ "vite": "^4.2.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-0.34.6.tgz", + "integrity": "sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.5", + "magic-string": "^0.30.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": ">=0.32.0 <1" + } + }, + "node_modules/@vitest/expect": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", + "dev": true, + "dependencies": { + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", + "dev": true, + "dependencies": { + "@vitest/utils": "0.34.6", + "p-limit": "^4.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", + "dev": true, + "dependencies": { + "tinyspy": "^2.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.4.3", + "loupe": "^2.3.6", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/acorn": { "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", @@ -1346,6 +1857,29 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", + "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1568,6 +2102,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -1583,6 +2126,14 @@ "has-symbols": "^1.0.3" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/autoprefixer": { "version": "10.4.16", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", @@ -1765,6 +2316,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", @@ -1817,6 +2377,24 @@ } ] }, + "node_modules/chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -1831,6 +2409,18 @@ "node": ">=4" } }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -1922,6 +2512,20 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", @@ -1963,6 +2567,12 @@ "node": ">= 8" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1975,6 +2585,20 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", @@ -1987,6 +2611,22 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2004,6 +2644,55 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-equal": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", + "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.1", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2137,6 +2826,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2152,6 +2852,15 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2182,6 +2891,26 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2213,6 +2942,20 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.22.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", @@ -2266,6 +3009,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-iterator-helpers": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", @@ -3164,7 +3927,23 @@ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", "dev": true, "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, "node_modules/fraction.js": { @@ -3245,6 +4024,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -3400,6 +4188,22 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/happy-dom": { + "version": "12.10.3", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-12.10.3.tgz", + "integrity": "sha512-JzUXOh0wdNGY54oKng5hliuBkq/+aT1V3YpTM+lrN/GoLQTANZsMaIvmHiHe612rauHvPJnDZkZ+5GZR++1Abg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "css.escape": "^1.5.1", + "entities": "^4.5.0", + "iconv-lite": "^0.6.3", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0" + } + }, "node_modules/has": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", @@ -3490,6 +4294,57 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -3514,6 +4369,20 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -3548,6 +4417,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3578,6 +4456,22 @@ "node": ">= 0.4" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -3833,6 +4727,14 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -4000,6 +4902,77 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.1.tgz", + "integrity": "sha512-opCrKqbthmq3SKZ10mFMQG9dk3fTa3quaOLD35kJa5ejwZHd9xAr+kLuziiZz2cG32s4lMZxNdmdcEQnTDP4+g==", + "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-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", @@ -4039,6 +5012,50 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -4081,6 +5098,12 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -4209,6 +5232,18 @@ "node": ">=16.0.0" } }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4234,6 +5269,12 @@ "virtual-scroll": "^1.5.2" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4297,6 +5338,15 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4306,6 +5356,42 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "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/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4334,6 +5420,31 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -4346,6 +5457,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4367,6 +5487,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mlly": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "ufo": "^1.3.0" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4459,6 +5591,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4485,6 +5625,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -4686,6 +5842,20 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4728,6 +5898,21 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -4776,6 +5961,17 @@ "node": ">= 6" } }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -5015,6 +6211,38 @@ } } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5026,6 +6254,14 @@ "react-is": "^16.13.1" } }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -5035,6 +6271,14 @@ "node": ">=6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5144,6 +6388,19 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -5187,6 +6444,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -5315,6 +6580,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/run-applescript": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", @@ -5492,6 +6765,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -5597,6 +6892,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -5651,6 +6952,15 @@ "resolved": "https://registry.npmjs.org/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.4.tgz", "integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==" }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -5660,6 +6970,30 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", + "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", + "dev": true + }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -5811,6 +7145,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5823,6 +7169,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/sucrase": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", @@ -5898,6 +7256,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/synckit": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", @@ -5960,6 +7326,20 @@ "node": ">=6" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5992,6 +7372,30 @@ "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-1.2.0.tgz", "integrity": "sha512-rWjF00inHeWtT5UbQYAXoMI4hL6TRMqohuKCsODyPYYmfAxqfMnXLsIeNrbdPEkNxlk++rojVilTnI9IVmEBtA==" }, + "node_modules/tinybench": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", + "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -6025,6 +7429,37 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -6085,6 +7520,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", @@ -6175,6 +7619,12 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", + "dev": true + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -6190,6 +7640,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -6238,12 +7705,38 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/v8-to-istanbul": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", + "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/virtual-scroll": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/virtual-scroll/-/virtual-scroll-1.5.2.tgz", @@ -6310,6 +7803,171 @@ } } }, + "node_modules/vite-node": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.4.0", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.5", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "acorn": "^8.9.0", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.10", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*", + "playwright": "*", + "safaridriver": "*", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6401,6 +8059,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -6463,6 +8137,48 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index d314422..6254071 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "react-ts-tw-template", + "name": "cinemania", "private": true, "version": "0.0.0", "type": "module", @@ -11,7 +11,8 @@ "type-check": "tsc --noEmit", "format": "prettier --write \"src/**/*.{ts,tsx,css}\"", "prepare": "husky install", - "precommit": "pnpm lint-staged && pnpm type-check" + "precommit": "pnpm lint-staged && pnpm type-check", + "test": "vitest --coverage" }, "dependencies": { "locomotive-scroll": "^4.1.4", @@ -20,12 +21,18 @@ "react-router-dom": "^6.18.0" }, "devDependencies": { + "@edge-runtime/vm": "^3.1.7", + "@testing-library/dom": "^9.3.3", + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.5.1", "@types/locomotive-scroll": "^4.1.2", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.0.3", + "@vitest/coverage-v8": "^0.34.6", "autoprefixer": "^10.4.16", "eslint": "^8.50.0", "eslint-config-airbnb": "^19.0.4", @@ -47,6 +54,7 @@ "prettier-plugin-tailwindcss": "^0.5.4", "tailwindcss": "^3.3.3", "typescript": "^5.2.2", - "vite": "^4.4.9" + "vite": "^4.4.9", + "vitest": "^0.34.6" } } From 42df70a5dc75e2a03eae3174b28be8acadf34dc3 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 7 Nov 2023 21:01:04 +0200 Subject: [PATCH 05/69] chore: configure vitest --- src/pages/AppLayout/AppLayout.test.tsx | 14 +++++++++++++ src/shared/lib/helpers/RenderWithRouter.tsx | 22 +++++++++++++++++++++ src/test/setup.ts | 3 +++ tsconfig.json | 3 ++- vite.config.ts | 13 +++++++++--- 5 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 src/pages/AppLayout/AppLayout.test.tsx create mode 100644 src/shared/lib/helpers/RenderWithRouter.tsx create mode 100644 src/test/setup.ts diff --git a/src/pages/AppLayout/AppLayout.test.tsx b/src/pages/AppLayout/AppLayout.test.tsx new file mode 100644 index 0000000..acdd025 --- /dev/null +++ b/src/pages/AppLayout/AppLayout.test.tsx @@ -0,0 +1,14 @@ +import { screen } from '@testing-library/react'; +import { describe, it } from 'vitest'; + +import renderWithRouter from '../../shared/lib/helpers/RenderWithRouter.tsx'; + +describe('App', () => { + it('Renders the app', async () => { + // 1. arrange + renderWithRouter(); + // 2. act + // 3. expect + expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument(); + }); +}); diff --git a/src/shared/lib/helpers/RenderWithRouter.tsx b/src/shared/lib/helpers/RenderWithRouter.tsx new file mode 100644 index 0000000..eac3d98 --- /dev/null +++ b/src/shared/lib/helpers/RenderWithRouter.tsx @@ -0,0 +1,22 @@ +import { ReactNode } from 'react'; + +import { render } from '@testing-library/react'; +import { createMemoryRouter, RouterProvider } from 'react-router-dom'; + +import { ROUTES } from '../../../app/router.tsx'; + +function RenderWithRouter( + initialEntries?: string[], + initialIndex?: number, + element?: ReactNode, +) { + const routes = element ? [{ path: '/', element }] : ROUTES; + + const router = createMemoryRouter(routes, { + initialEntries, + initialIndex, + }); + render(); +} + +export default RenderWithRouter; diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..cbe314e --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,3 @@ +import * as matchers from '@testing-library/jest-dom/matchers'; + +expect.extend(matchers); diff --git a/tsconfig.json b/tsconfig.json index b4e1e07..c372cd4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,8 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "types": ["vitest/globals", "@testing-library/jest-dom"] }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/vite.config.ts b/vite.config.ts index 86a5fa0..27f8747 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,15 @@ -import react from '@vitejs/plugin-react'; +/// +/// + import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], - base: './', + test: { + globals: true, + environment: 'happy-dom', + setupFiles: './src/test/setup.ts', + css: false, + }, }); From df2ba7a45df5110c429e13b8ae80d9d15e9b303b Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 8 Nov 2023 10:13:41 +0200 Subject: [PATCH 06/69] fix: remove event listener from the document on unmount --- src/shared/hooks/useTooltip.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/shared/hooks/useTooltip.ts b/src/shared/hooks/useTooltip.ts index 4590f5f..e6c7ed2 100644 --- a/src/shared/hooks/useTooltip.ts +++ b/src/shared/hooks/useTooltip.ts @@ -30,16 +30,15 @@ function useTooltip(scroll: RefObject) { } useEffect(() => { - if (scroll.current) - scroll.current.on?.('scroll', () => { - if (tooltipRef.current) tooltipRef.current.classList.add(...HIDDEN); - }); + scroll.current?.on?.( + 'scroll', + () => tooltipRef.current?.classList.add(...HIDDEN), + ); }, [scroll]); useEffect(() => { - document.body.addEventListener('mousemove', (e) => { - moveTooltip(e); - }); + document.addEventListener('mousemove', moveTooltip); + return () => document.removeEventListener('mousemove', moveTooltip); }, []); return { From 4be7a1d1819ee09ac62cca4a39b4a00166f4bf0e Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 8 Nov 2023 10:42:19 +0200 Subject: [PATCH 07/69] refactor: encapsulate mouse coord calculation in separate function --- src/shared/hooks/useTooltip.ts | 17 ++++++------ src/shared/lib/helpers/animateRadialHover.ts | 13 ++++----- .../lib/helpers/getElementMouseCoord.ts | 27 +++++++++++++++++++ 3 files changed, 40 insertions(+), 17 deletions(-) create mode 100644 src/shared/lib/helpers/getElementMouseCoord.ts diff --git a/src/shared/hooks/useTooltip.ts b/src/shared/hooks/useTooltip.ts index e6c7ed2..297f7c7 100644 --- a/src/shared/hooks/useTooltip.ts +++ b/src/shared/hooks/useTooltip.ts @@ -2,23 +2,22 @@ import { RefObject, useEffect, useRef } from 'react'; import LocomotiveScroll from 'locomotive-scroll'; -const HIDDEN = ['invisible', 'opacity-0', '!scale-[.3]', 'text-lime-500']; +import getElementMouseCoord from '../lib/helpers/getElementMouseCoord.ts'; + +const HIDDEN = ['invisible', 'opacity-0', '!scale-[.3]']; +const ELEMENT_POSITION_OFFSET = 120; function useTooltip(scroll: RefObject) { const tooltipRef = useRef(null); function moveTooltip(e: MouseEvent) { - const screenCoord = document.body.getBoundingClientRect(); - - if (!screenCoord) return; + const { posX, posY } = getElementMouseCoord(document.body, e); - const posX = e.clientX - screenCoord.x - 120; - const posY = e.clientY - screenCoord.y - 120; + const pointerX = posX - ELEMENT_POSITION_OFFSET; + const pointerY = posY - ELEMENT_POSITION_OFFSET; if (tooltipRef.current) - tooltipRef.current.style.cssText = ` - translate: ${posX}px ${posY}px; - `; + tooltipRef.current.style.translate = `${pointerX}px ${pointerY}px`; } function showTooltip() { diff --git a/src/shared/lib/helpers/animateRadialHover.ts b/src/shared/lib/helpers/animateRadialHover.ts index f98aabe..2228d38 100644 --- a/src/shared/lib/helpers/animateRadialHover.ts +++ b/src/shared/lib/helpers/animateRadialHover.ts @@ -2,21 +2,18 @@ import { MouseEvent } from 'react'; import colors from 'tailwindcss/colors'; +import getElementMouseCoord from './getElementMouseCoord.ts'; + type AnimationFn = (elem: HTMLElement, evt: MouseEvent) => void; type CleanUpFn = (elem: HTMLElement) => void; type RadialHover = [AnimationFn, CleanUpFn]; -function animateRadialHover(elem: HTMLElement, evt: MouseEvent) { - const rect = elem.getBoundingClientRect(); - - if (rect) { - const pointerX = evt.clientX - rect.left; - const pointerY = evt.clientY - rect.top; +function animateRadialHover(elem: HTMLElement, e: MouseEvent) { + const { posX, posY } = getElementMouseCoord(elem, e); - elem.style.background = `radial-gradient(circle at ${pointerX}px ${pointerY}px, rgb(112, 26, 117, 0.5) 0%, ${colors.transparent} 160px)`; - } + elem.style.background = `radial-gradient(circle at ${posX}px ${posY}px, rgb(112, 26, 117, 0.5) 0%, ${colors.transparent} 160px)`; } function cleanUp(elem: HTMLElement) { diff --git a/src/shared/lib/helpers/getElementMouseCoord.ts b/src/shared/lib/helpers/getElementMouseCoord.ts new file mode 100644 index 0000000..2bfbc2c --- /dev/null +++ b/src/shared/lib/helpers/getElementMouseCoord.ts @@ -0,0 +1,27 @@ +import { MouseEvent as ReactMouseEvent } from 'react'; + +/** + * Calculates the mouse coordinates relative to an HTML element. + * + * @param {TElem} elem - The HTML element to calculate the coordinates relative to. + * @param {MouseEvent | ReactMouseEvent} e - The mouse event object containing the client coordinates. + * @throws {Error} - If the element's bounding rectangle cannot be obtained. + * @return mouseCoord - An object containing the X and Y coordinates of the mouse relative to the element. + * @return mouseCoord.posX {number} - Position X. + * @return mouseCoord.posY {number} - Position Y. + */ +function getElementMouseCoord( + elem: TElem, + e: MouseEvent | ReactMouseEvent, +) { + const elemBCR = elem.getBoundingClientRect(); + + if (!elemBCR) throw new Error('Cannot get elements bounding rect!'); + + const posX = e.clientX - elemBCR.x; + const posY = e.clientY - elemBCR.y; + + return { posX, posY }; +} + +export default getElementMouseCoord; From d5833fcca69e5ca81a3acd73626fc25ec2cd09f4 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 8 Nov 2023 11:01:42 +0200 Subject: [PATCH 08/69] chore: add tailwindMerge & clsx libraries --- package-lock.json | 28 ++++++++++++++++++++++++---- package.json | 4 +++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ec4bf9..cc27f58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,12 @@ "name": "cinemania", "version": "0.0.0", "dependencies": { + "clsx": "^2.0.0", "locomotive-scroll": "^4.1.4", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react-router-dom": "^6.18.0", + "tailwind-merge": "^2.0.0" }, "devDependencies": { "@edge-runtime/vm": "^3.1.7", @@ -391,7 +393,6 @@ "version": "7.23.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2491,6 +2492,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -6424,8 +6433,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", @@ -7280,6 +7288,18 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tailwind-merge": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.0.0.tgz", + "integrity": "sha512-WO8qghn9yhsldLSg80au+3/gY9E4hFxIvQ3qOmlpXnqpDKoMruKfi/56BbbMg6fHTQJ9QD3cc79PoWqlaQE4rw==", + "dependencies": { + "@babel/runtime": "^7.23.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", diff --git a/package.json b/package.json index 6254071..e0b32ef 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,12 @@ "test": "vitest --coverage" }, "dependencies": { + "clsx": "^2.0.0", "locomotive-scroll": "^4.1.4", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react-router-dom": "^6.18.0", + "tailwind-merge": "^2.0.0" }, "devDependencies": { "@edge-runtime/vm": "^3.1.7", From f1f9dc202ee9810bf3d2907bab9aa54971cf17b6 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 8 Nov 2023 11:03:24 +0200 Subject: [PATCH 09/69] fix: classes conflicts in Button component --- src/shared/lib/helpers/cn.ts | 8 ++++++++ src/shared/ui/Button.tsx | 31 +++++++++++++++---------------- src/shared/ui/Tabs.tsx | 2 +- 3 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 src/shared/lib/helpers/cn.ts diff --git a/src/shared/lib/helpers/cn.ts b/src/shared/lib/helpers/cn.ts new file mode 100644 index 0000000..3c96163 --- /dev/null +++ b/src/shared/lib/helpers/cn.ts @@ -0,0 +1,8 @@ +import { ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export default cn; diff --git a/src/shared/ui/Button.tsx b/src/shared/ui/Button.tsx index 47d38e1..652f4f1 100644 --- a/src/shared/ui/Button.tsx +++ b/src/shared/ui/Button.tsx @@ -1,37 +1,36 @@ -import { memo, MouseEvent } from 'react'; +import { ButtonHTMLAttributes, memo, PropsWithChildren } from 'react'; -import { IChildren } from '../types/interfaces.ts'; +import cn from '../lib/helpers/cn.ts'; const buttonTypes = { filled: - 'disabled:opacity-20 disabled:hover:bg-lime-400 disabled:hover:scale-100 disabled:active:scale-100 rounded-full bg-lime-400 px-4 py-3 font-semibold text-gray-950 transition-all duration-200 hover:scale-110 focus:outline-0 focus:ring focus:ring-lime-300 focus:ring-offset-2 focus:ring-offset-black/70 sm:px-6 active:scale-100 active:duration-75', + 'disabled:hover:bg-lime-400 rounded-full bg-lime-400 px-4 py-3 font-semibold text-gray-950 transition-all duration-200 hover:scale-110 focus:outline-0 focus:ring focus:ring-lime-300 focus:ring-offset-2 focus:ring-offset-black/70 sm:px-6 active:scale-100 active:duration-75', empty: - 'disabled:opacity-20 disabled:hover:bg-lime-400 disabled:hover:scale-100 disabled:active:scale-100 border border-lime-300 rounded-full px-4 py-3 font-semibold text-lime-300 transition-all duration-200 hover:bg-lime-200/30 focus:outline-0 focus:ring focus:ring-lime-300 focus:ring-offset-2 focus:ring-offset-black/70 sm:px-6', + 'disabled:hover:bg-lime-400 disabled:active:scale-100 border border-lime-300 rounded-full px-4 py-3 font-semibold text-lime-300 transition-all duration-200 hover:bg-lime-200/30 focus:outline-0 focus:ring focus:ring-lime-300 focus:ring-offset-2 focus:ring-offset-black/70 sm:px-6', select: 'rounded-full relative py-2 px-4 transition-all before:top-0 before:pointer-events-none before:absolute before:left-0 before:right-0 before:-z-10 before:m-auto before:h-full before:w-full before:scale-75 before:rounded-full before:bg-lime-400/50 before:opacity-0 before:transition-all hover:before:scale-100 hover:before:opacity-100 duration-250 w-[112px]', }; -interface IButtonProps extends IChildren { - onClick?: (e: MouseEvent) => void; - type?: keyof typeof buttonTypes; - className?: string; - disabled?: boolean; +interface IButtonProps + extends PropsWithChildren, + ButtonHTMLAttributes { + styleType?: keyof typeof buttonTypes; } const Button = memo(function Button({ - type = 'filled', - onClick, - className = '', - disabled = false, + styleType = 'filled', children, + className, + disabled, ...props }: IButtonProps) { return ( diff --git a/src/shared/ui/Tabs.tsx b/src/shared/ui/Tabs.tsx index c001169..f08d8b2 100644 --- a/src/shared/ui/Tabs.tsx +++ b/src/shared/ui/Tabs.tsx @@ -68,7 +68,7 @@ function Tab({ return ( ); -} +}); export default Button; diff --git a/src/pages/AppLayout/AppLayout.tsx b/src/pages/AppLayout/AppLayout.tsx index 98c8aed..c4a0e4b 100644 --- a/src/pages/AppLayout/AppLayout.tsx +++ b/src/pages/AppLayout/AppLayout.tsx @@ -1,11 +1,16 @@ import { Outlet } from 'react-router-dom'; import GradientBackground from './ui/GradientBackground.tsx'; +import Movie from '../../entities/movie/ui/Movie.tsx'; import MovieList from '../../features/MovieList/MovieList.tsx'; +import MovieListHeader from '../../features/MovieList/ui/MovieListHeader.tsx'; +import PageNum from '../../features/MovieList/ui/PageNum.tsx'; import Pagination from '../../features/Pagination/Pagination.tsx'; import SearchProvider from '../../features/Search/context/SearchProvider.tsx'; import Search from '../../features/Search/Search.tsx'; import useScroll from '../../shared/hooks/useScroll.ts'; +import useTooltip from '../../shared/hooks/useTooltip.ts'; +import Tooltip from '../../shared/ui/Tooltip.tsx'; import Header from '../../widgets/Header/Header.tsx'; import Logo from '../../widgets/Header/ui/Logo.tsx'; import TotalResults from '../../widgets/Header/ui/TotalResults.tsx'; @@ -14,6 +19,7 @@ import Main from '../../widgets/Main/Main.tsx'; function AppLayout() { const { containerRef, scrollRef } = useScroll(); + const { tooltipRef, hideTooltip, showTooltip } = useTooltip(scrollRef); return ( @@ -23,6 +29,7 @@ function AppLayout() { ref={containerRef} className="relative m-auto min-h-screen">
+ Click for details
@@ -31,10 +38,24 @@ function AppLayout() {
- + ( + + )}> + + + +
- +
diff --git a/src/shared/hooks/useTooltip.ts b/src/shared/hooks/useTooltip.ts index 297f7c7..417bc62 100644 --- a/src/shared/hooks/useTooltip.ts +++ b/src/shared/hooks/useTooltip.ts @@ -1,4 +1,4 @@ -import { RefObject, useEffect, useRef } from 'react'; +import { RefObject, useCallback, useEffect, useRef } from 'react'; import LocomotiveScroll from 'locomotive-scroll'; @@ -10,7 +10,7 @@ const ELEMENT_POSITION_OFFSET = 120; function useTooltip(scroll: RefObject) { const tooltipRef = useRef(null); - function moveTooltip(e: MouseEvent) { + const moveTooltip = useCallback((e: MouseEvent) => { const { posX, posY } = getElementMouseCoord(document.body, e); const pointerX = posX - ELEMENT_POSITION_OFFSET; @@ -18,15 +18,15 @@ function useTooltip(scroll: RefObject) { if (tooltipRef.current) tooltipRef.current.style.translate = `${pointerX}px ${pointerY}px`; - } + }, []); - function showTooltip() { + const showTooltip = useCallback(() => { tooltipRef.current?.classList.remove(...HIDDEN); - } + }, []); - function hideTooltip() { + const hideTooltip = useCallback(() => { tooltipRef.current?.classList.add(...HIDDEN); - } + }, []); useEffect(() => { scroll.current?.on?.( @@ -38,7 +38,7 @@ function useTooltip(scroll: RefObject) { useEffect(() => { document.addEventListener('mousemove', moveTooltip); return () => document.removeEventListener('mousemove', moveTooltip); - }, []); + }, [moveTooltip]); return { tooltipRef, diff --git a/src/shared/hooks/useUrl.ts b/src/shared/hooks/useUrl.ts index 89ff629..289e793 100644 --- a/src/shared/hooks/useUrl.ts +++ b/src/shared/hooks/useUrl.ts @@ -2,6 +2,14 @@ import { useCallback } from 'react'; import { useSearchParams } from 'react-router-dom'; +/* +TODO - add default params +{ + [PAGE_PARAM]: String(DEFAULT_PAGE), + [MOVIES_PER_PAGE_PARAM]: String(DEFAULT_MOVIES_PER_PAGE), +} + */ + function useUrl() { const [searchParams, setSearchParams] = useSearchParams(); diff --git a/src/shared/ui/Tabs.tsx b/src/shared/ui/Tabs.tsx index d20bdab..c798c51 100644 --- a/src/shared/ui/Tabs.tsx +++ b/src/shared/ui/Tabs.tsx @@ -1,5 +1,6 @@ import { createContext, + memo, MouseEvent, PropsWithChildren, useCallback, @@ -10,7 +11,7 @@ import { import Button from './Button.tsx'; interface IOptionProps { - children: string | string[]; + children: string; value: TVal; } @@ -76,6 +77,6 @@ function Tab({ ); } -Tabs.Tab = Tab; +Tabs.Tab = memo(Tab) as typeof Tab; export default Tabs; From 72552652ef1f94b276f56516d5aab17be50713e1 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 00:12:43 +0200 Subject: [PATCH 33/69] refactor: divide component details into smaller ui components --- src/app/router.tsx | 7 ++- src/shared/hooks/useDocumentTitle.ts | 14 ++++++ src/widgets/MovieDetails/MovieDetails.tsx | 52 ++++++++------------- src/widgets/MovieDetails/hooks/useMovie.ts | 16 ++----- src/widgets/MovieDetails/ui/Actors.tsx | 12 +++++ src/widgets/MovieDetails/ui/Desctiption.tsx | 11 +++++ src/widgets/MovieDetails/ui/Director.tsx | 12 +++++ src/widgets/MovieDetails/ui/Genre.tsx | 7 +++ src/widgets/MovieDetails/ui/Poster.tsx | 17 +++++++ src/widgets/MovieDetails/ui/Rating.tsx | 14 ++++++ src/widgets/MovieDetails/ui/Runtime.tsx | 14 ++++++ src/widgets/MovieDetails/ui/Title.tsx | 13 ++++++ 12 files changed, 143 insertions(+), 46 deletions(-) create mode 100644 src/shared/hooks/useDocumentTitle.ts create mode 100644 src/widgets/MovieDetails/ui/Actors.tsx create mode 100644 src/widgets/MovieDetails/ui/Desctiption.tsx create mode 100644 src/widgets/MovieDetails/ui/Director.tsx create mode 100644 src/widgets/MovieDetails/ui/Genre.tsx create mode 100644 src/widgets/MovieDetails/ui/Poster.tsx create mode 100644 src/widgets/MovieDetails/ui/Rating.tsx create mode 100644 src/widgets/MovieDetails/ui/Runtime.tsx create mode 100644 src/widgets/MovieDetails/ui/Title.tsx diff --git a/src/app/router.tsx b/src/app/router.tsx index 48a4142..6f4b84e 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -3,6 +3,7 @@ import { createHashRouter } from 'react-router-dom'; import loader from '../entities/movie/loader.ts'; import AppLayout from '../pages/AppLayout/AppLayout.tsx'; import MovieDetails from '../widgets/MovieDetails/MovieDetails.tsx'; +import BackButton from '../widgets/MovieDetails/ui/BackButton.tsx'; export const ROUTES = [ { @@ -10,7 +11,11 @@ export const ROUTES = [ path: '/', children: [ { - element: , + element: ( + + + + ), path: ':movieId', loader, }, diff --git a/src/shared/hooks/useDocumentTitle.ts b/src/shared/hooks/useDocumentTitle.ts new file mode 100644 index 0000000..60d887f --- /dev/null +++ b/src/shared/hooks/useDocumentTitle.ts @@ -0,0 +1,14 @@ +import { useEffect } from 'react'; + +import { APP_TITLE } from '../const/const.ts'; + +function useDocumentTitle(newTitle: string) { + useEffect(() => { + if (newTitle) document.title = newTitle; + return () => { + document.title = APP_TITLE; + }; + }, [newTitle]); +} + +export default useDocumentTitle; diff --git a/src/widgets/MovieDetails/MovieDetails.tsx b/src/widgets/MovieDetails/MovieDetails.tsx index e6f3822..68ca314 100644 --- a/src/widgets/MovieDetails/MovieDetails.tsx +++ b/src/widgets/MovieDetails/MovieDetails.tsx @@ -1,7 +1,16 @@ +import { PropsWithChildren } from 'react'; + import useMovie from './hooks/useMovie.ts'; -import BackButton from './ui/BackButton.tsx'; +import Actors from './ui/Actors.tsx'; +import Desctiption from './ui/Desctiption.tsx'; +import Director from './ui/Director.tsx'; +import Genre from './ui/Genre.tsx'; +import Poster from './ui/Poster.tsx'; +import Rating from './ui/Rating.tsx'; +import Runtime from './ui/Runtime.tsx'; +import Title from './ui/Title.tsx'; -function MovieDetails() { +function MovieDetails({ children }: PropsWithChildren) { const { description, imdbRating, @@ -23,37 +32,16 @@ function MovieDetails() { data-scroll-target="section" className="h-[540px] flex-1 animate-fade-in overflow-hidden rounded-5xl border-l border-t border-white/20 bg-white/10 p-2 text-neutral-200 shadow-2xl backdrop-brightness-150 backdrop-saturate-200">
- {`The +
- -

- {title} -

- - {time} | {year} - - {genre} - - ⭐{imdbRating}/10 | 🍿{imdbVotes} - -

- {description} -

-

- Directed By: - {director} -

-

- Cast: - {actors} -

+ {children} + {title} + + {genre} + + {description} + {director} + {actors}
diff --git a/src/widgets/MovieDetails/hooks/useMovie.ts b/src/widgets/MovieDetails/hooks/useMovie.ts index 4bd2ad5..f164821 100644 --- a/src/widgets/MovieDetails/hooks/useMovie.ts +++ b/src/widgets/MovieDetails/hooks/useMovie.ts @@ -1,24 +1,14 @@ -import { useEffect } from 'react'; - import { useLoaderData } from 'react-router-dom'; import ReactLogo from '../../../assets/reactJS-logo.png'; -import { APP_TITLE, NOT_EXIST } from '../../../shared/const/const.ts'; +import { NOT_EXIST } from '../../../shared/const/const.ts'; +import useDocumentTitle from '../../../shared/hooks/useDocumentTitle.ts'; import convertSecsToHrsAndMins from '../../../shared/lib/helpers/convertSecsToHrsAndMins.ts'; import { ApiMovieResponse } from '../../../shared/types/types.ts'; function useMovie() { const movie = useLoaderData() as ApiMovieResponse; - - useEffect(() => { - const newTitle = movie.Title; - - if (newTitle) document.title = `Cinemania | ${newTitle}`; - - return () => { - document.title = APP_TITLE; - }; - }, [movie.Title]); + useDocumentTitle(`Cinemania | ${movie.Title}`); const { Poster, diff --git a/src/widgets/MovieDetails/ui/Actors.tsx b/src/widgets/MovieDetails/ui/Actors.tsx new file mode 100644 index 0000000..0016e2d --- /dev/null +++ b/src/widgets/MovieDetails/ui/Actors.tsx @@ -0,0 +1,12 @@ +import { PropsWithChildren } from 'react'; + +function Actors({ children }: PropsWithChildren) { + return ( +

+ Cast: + {children} +

+ ); +} + +export default Actors; diff --git a/src/widgets/MovieDetails/ui/Desctiption.tsx b/src/widgets/MovieDetails/ui/Desctiption.tsx new file mode 100644 index 0000000..f8d0f34 --- /dev/null +++ b/src/widgets/MovieDetails/ui/Desctiption.tsx @@ -0,0 +1,11 @@ +import { PropsWithChildren } from 'react'; + +function Desctiption({ children }: PropsWithChildren) { + return ( +

+ {children} +

+ ); +} + +export default Desctiption; diff --git a/src/widgets/MovieDetails/ui/Director.tsx b/src/widgets/MovieDetails/ui/Director.tsx new file mode 100644 index 0000000..e6bf138 --- /dev/null +++ b/src/widgets/MovieDetails/ui/Director.tsx @@ -0,0 +1,12 @@ +import { PropsWithChildren } from 'react'; + +function Director({ children }: PropsWithChildren) { + return ( +

+ Directed By: + {children} +

+ ); +} + +export default Director; diff --git a/src/widgets/MovieDetails/ui/Genre.tsx b/src/widgets/MovieDetails/ui/Genre.tsx new file mode 100644 index 0000000..21017b2 --- /dev/null +++ b/src/widgets/MovieDetails/ui/Genre.tsx @@ -0,0 +1,7 @@ +import { PropsWithChildren } from 'react'; + +function Genre({ children }: PropsWithChildren) { + return {children}; +} + +export default Genre; diff --git a/src/widgets/MovieDetails/ui/Poster.tsx b/src/widgets/MovieDetails/ui/Poster.tsx new file mode 100644 index 0000000..b6b2d3a --- /dev/null +++ b/src/widgets/MovieDetails/ui/Poster.tsx @@ -0,0 +1,17 @@ +interface IMoviePosterProps { + poster: string; + title: string; +} + +function Poster({ poster, title }: IMoviePosterProps) { + return ( + {`The + ); +} + +export default Poster; diff --git a/src/widgets/MovieDetails/ui/Rating.tsx b/src/widgets/MovieDetails/ui/Rating.tsx new file mode 100644 index 0000000..1f18af9 --- /dev/null +++ b/src/widgets/MovieDetails/ui/Rating.tsx @@ -0,0 +1,14 @@ +interface IMovieRating { + rating: string; + votes: string; +} + +function Rating({ rating, votes }: IMovieRating) { + return ( + + ⭐{rating}10 | 🍿{votes} + + ); +} + +export default Rating; diff --git a/src/widgets/MovieDetails/ui/Runtime.tsx b/src/widgets/MovieDetails/ui/Runtime.tsx new file mode 100644 index 0000000..9c91170 --- /dev/null +++ b/src/widgets/MovieDetails/ui/Runtime.tsx @@ -0,0 +1,14 @@ +interface IMovieRuntime { + time: string; + year: string; +} + +function Runtime({ time, year }: IMovieRuntime) { + return ( + + {time} | {year} + + ); +} + +export default Runtime; diff --git a/src/widgets/MovieDetails/ui/Title.tsx b/src/widgets/MovieDetails/ui/Title.tsx new file mode 100644 index 0000000..6701cb8 --- /dev/null +++ b/src/widgets/MovieDetails/ui/Title.tsx @@ -0,0 +1,13 @@ +import { PropsWithChildren } from 'react'; + +function Title({ children }: PropsWithChildren) { + return ( +

+ {children} +

+ ); +} + +export default Title; From b7b34b682192a958d118cc06c25230cfcbaee30f Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 09:42:47 +0200 Subject: [PATCH 34/69] refactor: move useScrollTop to usePagination hook --- src/features/Pagination/Pagination.tsx | 10 +--------- src/features/Pagination/hooks/usePagination.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/features/Pagination/Pagination.tsx b/src/features/Pagination/Pagination.tsx index 8481818..78f50c3 100644 --- a/src/features/Pagination/Pagination.tsx +++ b/src/features/Pagination/Pagination.tsx @@ -6,9 +6,6 @@ import usePagination from './hooks/usePagination.ts'; import Button from './ui/Button.tsx'; import arrowLeft from '../../assets/arrow-left.svg'; import arrowRight from '../../assets/arrow-right.svg'; -import { PAGE_PARAM } from '../../shared/const/const.ts'; -import useScrollTop from '../../shared/hooks/useScrollTop.ts'; -import useUrl from '../../shared/hooks/useUrl.ts'; interface IPaginationProps { scroll: RefObject; @@ -21,12 +18,7 @@ function Pagination({ scroll }: IPaginationProps) { isPrevDisabled, isNextDisabled, noPages, - } = usePagination(); - const { readUrl } = useUrl(); - - const currPage = Number(readUrl(PAGE_PARAM)); - - useScrollTop(currPage, scroll, undefined, currPage); + } = usePagination(scroll); if (noPages) return null; diff --git a/src/features/Pagination/hooks/usePagination.ts b/src/features/Pagination/hooks/usePagination.ts index 819f166..b5fcd9c 100644 --- a/src/features/Pagination/hooks/usePagination.ts +++ b/src/features/Pagination/hooks/usePagination.ts @@ -1,10 +1,13 @@ -import { useCallback } from 'react'; +import { RefObject, useCallback } from 'react'; + +import LocomotiveScroll from 'locomotive-scroll'; import { DEFAULT_PAGE, PAGE_PARAM } from '../../../shared/const/const.ts'; +import useScrollTop from '../../../shared/hooks/useScrollTop.ts'; import useUrl from '../../../shared/hooks/useUrl.ts'; import useSearch from '../../Search/hooks/useSearch.ts'; -function usePagination() { +function usePagination(scroll: RefObject) { const { setUrl, readUrl } = useUrl(); const { fetchMovies, query, isLoading, totalResults } = useSearch(); @@ -14,6 +17,8 @@ function usePagination() { const isPage = Boolean(currPage); const noPages = totalResults <= 10; + useScrollTop(currPage, scroll, undefined, currPage); + const handleNextPage = useCallback(() => { const newPage = currPage + 1; From 5bb4bc9da254a551fa2ac2bab03554bb10d19fbe Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 10:05:00 +0200 Subject: [PATCH 35/69] fix: navigation issue --- src/features/Search/context/SearchProvider.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/features/Search/context/SearchProvider.tsx b/src/features/Search/context/SearchProvider.tsx index 1a4858b..5f03310 100644 --- a/src/features/Search/context/SearchProvider.tsx +++ b/src/features/Search/context/SearchProvider.tsx @@ -96,9 +96,10 @@ function SearchProvider({ children }: IChildren) { useEffect(() => { const storedQuery = localStorage.getItem(LOCAL_STORAGE_SEARCH_QUERY); - const page = Number(readUrl(PAGE_PARAM)) || DEFAULT_PAGE; + const urlPage = Number(readUrl(PAGE_PARAM)); + const page = urlPage || DEFAULT_PAGE; - setUrl(PAGE_PARAM, String(page)); + if (!urlPage) setUrl(PAGE_PARAM, String(page)); if (storedQuery === null) return; From 291373ace0bbc326289b63867df526f189637b01 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 11:34:50 +0200 Subject: [PATCH 36/69] fix: to not fetch movies 2 times on pagination --- .../Pagination/hooks/usePagination.ts | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/features/Pagination/hooks/usePagination.ts b/src/features/Pagination/hooks/usePagination.ts index b5fcd9c..1c0c05d 100644 --- a/src/features/Pagination/hooks/usePagination.ts +++ b/src/features/Pagination/hooks/usePagination.ts @@ -2,39 +2,33 @@ import { RefObject, useCallback } from 'react'; import LocomotiveScroll from 'locomotive-scroll'; -import { DEFAULT_PAGE, PAGE_PARAM } from '../../../shared/const/const.ts'; +import { + DEFAULT_MOVIES_PER_PAGE, + DEFAULT_PAGE, +} from '../../../shared/const/const.ts'; import useScrollTop from '../../../shared/hooks/useScrollTop.ts'; import useUrl from '../../../shared/hooks/useUrl.ts'; +import { urlParams } from '../../../shared/types/enums.ts'; import useSearch from '../../Search/hooks/useSearch.ts'; function usePagination(scroll: RefObject) { const { setUrl, readUrl } = useUrl(); - const { fetchMovies, query, isLoading, totalResults } = useSearch(); + const { isLoading, totalResults } = useSearch(); - const currPage = Number(readUrl(PAGE_PARAM)); - const isPrevDisabled = currPage === 1 || isLoading; + const currPage = Number(readUrl(urlParams.PAGE)); + const isPrevDisabled = currPage === DEFAULT_PAGE || isLoading; const isNextDisabled = isLoading; - const isPage = Boolean(currPage); - const noPages = totalResults <= 10; + const noPages = totalResults <= DEFAULT_MOVIES_PER_PAGE; useScrollTop(currPage, scroll, undefined, currPage); const handleNextPage = useCallback(() => { - const newPage = currPage + 1; - - setUrl(PAGE_PARAM, isPage ? String(newPage) : String(DEFAULT_PAGE)); - - fetchMovies(query, newPage); - }, [isPage, setUrl, fetchMovies, query, currPage]); + setUrl(urlParams.PAGE, currPage + 1); + }, [setUrl, currPage]); const handlePrevPage = useCallback(() => { - if (!isPage || currPage === 1) return; - - const newPage = currPage - 1; - - setUrl(PAGE_PARAM, String(newPage)); - fetchMovies(query, newPage); - }, [isPage, currPage, setUrl, fetchMovies, query]); + setUrl(urlParams.PAGE, currPage - 1); + }, [currPage, setUrl]); return { handleNextPage, From 29c35b7a0e02f6b785e1e87a07a6b7c43874e6ab Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 11:40:11 +0200 Subject: [PATCH 37/69] refactor: useUrl hook improvements and url issues fix --- src/features/MovieList/hooks/useListClick.ts | 4 +- src/features/MovieList/hooks/useMovieList.ts | 9 +--- src/features/MovieList/ui/MovieListHeader.tsx | 23 ++++----- src/features/MovieList/ui/PageNum.tsx | 12 ++--- src/features/Search/Search.tsx | 4 +- .../Search/context/SearchProvider.tsx | 6 +-- src/shared/const/const.ts | 8 +++- src/shared/hooks/useUrl.ts | 48 +++++++++++++------ src/shared/types/enums.ts | 7 ++- src/shared/types/types.ts | 3 +- 10 files changed, 67 insertions(+), 57 deletions(-) diff --git a/src/features/MovieList/hooks/useListClick.ts b/src/features/MovieList/hooks/useListClick.ts index da5c665..24caf41 100644 --- a/src/features/MovieList/hooks/useListClick.ts +++ b/src/features/MovieList/hooks/useListClick.ts @@ -5,10 +5,10 @@ import { useNavigate } from 'react-router-dom'; import { DEFAULT_PAGE, - PAGE_PARAM, SCROLL_TOP_DURATION, } from '../../../shared/const/const.ts'; import useUrl from '../../../shared/hooks/useUrl.ts'; +import { urlParams } from '../../../shared/types/enums.ts'; function useListClick(scroll: RefObject) { const listRef = useRef(null); @@ -17,7 +17,7 @@ function useListClick(scroll: RefObject) { function handleClick(e: MouseEvent) { const { target } = e; - const currPage = Number(readUrl(PAGE_PARAM)); + const currPage = Number(readUrl(urlParams.PAGE)); if (target !== listRef.current || currPage === DEFAULT_PAGE) return; diff --git a/src/features/MovieList/hooks/useMovieList.ts b/src/features/MovieList/hooks/useMovieList.ts index b476f7e..a26d108 100644 --- a/src/features/MovieList/hooks/useMovieList.ts +++ b/src/features/MovieList/hooks/useMovieList.ts @@ -1,17 +1,12 @@ -import { - DEFAULT_MOVIES_PER_PAGE, - MOVIES_PER_PAGE_PARAM, -} from '../../../shared/const/const.ts'; import useUrl from '../../../shared/hooks/useUrl.ts'; +import { urlParams } from '../../../shared/types/enums.ts'; import useSearch from '../../Search/hooks/useSearch.ts'; function useMovieList() { const { movies, isLoading } = useSearch(); const { readUrl } = useUrl(); - const moviesPerPage = Number( - readUrl(MOVIES_PER_PAGE_PARAM) || DEFAULT_MOVIES_PER_PAGE, - ); + const moviesPerPage = Number(readUrl(urlParams.MOVIES_PER_PAGE)); const noMovies = !movies?.length && !isLoading; const renderMovies = movies?.slice(0, moviesPerPage); diff --git a/src/features/MovieList/ui/MovieListHeader.tsx b/src/features/MovieList/ui/MovieListHeader.tsx index afb279e..b5a528a 100644 --- a/src/features/MovieList/ui/MovieListHeader.tsx +++ b/src/features/MovieList/ui/MovieListHeader.tsx @@ -2,14 +2,10 @@ import { PropsWithChildren, RefObject, useCallback } from 'react'; import LocomotiveScroll from 'locomotive-scroll'; -import { - DEFAULT_MOVIES_PER_PAGE, - DEFAULT_PAGE, - MOVIES_PER_PAGE_PARAM, - PAGE_PARAM, -} from '../../../shared/const/const.ts'; +import { DEFAULT_PAGE } from '../../../shared/const/const.ts'; import useScrollTop from '../../../shared/hooks/useScrollTop.ts'; import useUrl from '../../../shared/hooks/useUrl.ts'; +import { urlParams } from '../../../shared/types/enums.ts'; import { ItemsPerPage } from '../../../shared/types/types.ts'; import Tabs from '../../../shared/ui/Tabs.tsx'; @@ -22,20 +18,17 @@ function MovieListHeader({ children, scroll }: IMovieListHeader) { const handleMoviesPerPage = useCallback( (value: number) => { - setUrl(MOVIES_PER_PAGE_PARAM, String(value)); + setUrl({ + 'movies-per-page': String(value), + page: String(DEFAULT_PAGE), + }); }, [setUrl], ); - const moviesPerPage = Number( - readUrl(MOVIES_PER_PAGE_PARAM) || DEFAULT_MOVIES_PER_PAGE, - ); - - function resetPage() { - setUrl(PAGE_PARAM, String(DEFAULT_PAGE)); - } + const moviesPerPage = Number(readUrl(urlParams.MOVIES_PER_PAGE)); - useScrollTop(moviesPerPage, scroll, resetPage); + useScrollTop(moviesPerPage, scroll); return (
{ - setUrl(PAGE_PARAM, String(DEFAULT_PAGE)); + setUrl(urlParams.PAGE, String(DEFAULT_PAGE)); fetchMovies(newQuery.trim()); scroll?.current?.scrollTo('top', { duration: SCROLL_TOP_DURATION }); }, diff --git a/src/features/Search/context/SearchProvider.tsx b/src/features/Search/context/SearchProvider.tsx index 5f03310..3f8f382 100644 --- a/src/features/Search/context/SearchProvider.tsx +++ b/src/features/Search/context/SearchProvider.tsx @@ -10,9 +10,9 @@ import { getMovieList } from '../../../entities/movie/api/apiMovie.ts'; import { DEFAULT_PAGE, LOCAL_STORAGE_SEARCH_QUERY, - PAGE_PARAM, } from '../../../shared/const/const.ts'; import useUrl from '../../../shared/hooks/useUrl.ts'; +import { urlParams } from '../../../shared/types/enums.ts'; import { IChildren } from '../../../shared/types/interfaces.ts'; import { MovieList } from '../../../shared/types/types.ts'; import { NO_MOVIES, NO_RESULTS } from '../const/const.ts'; @@ -96,10 +96,10 @@ function SearchProvider({ children }: IChildren) { useEffect(() => { const storedQuery = localStorage.getItem(LOCAL_STORAGE_SEARCH_QUERY); - const urlPage = Number(readUrl(PAGE_PARAM)); + const urlPage = Number(readUrl(urlParams.PAGE)); const page = urlPage || DEFAULT_PAGE; - if (!urlPage) setUrl(PAGE_PARAM, String(page)); + if (!urlPage) setUrl(urlParams.PAGE, String(page)); if (storedQuery === null) return; diff --git a/src/shared/const/const.ts b/src/shared/const/const.ts index 0924b54..d3da512 100644 --- a/src/shared/const/const.ts +++ b/src/shared/const/const.ts @@ -1,13 +1,17 @@ +import { urlParams } from '../types/enums.ts'; + export const API_KEY = 'dbb72d83'; export const API_URL = `https://www.omdbapi.com/?apikey=${API_KEY}`; export const API_URL_NO_KEY = 'https://www.omdbapi.com/'; export const QUERY_FALLBACK = 'all'; export const NOT_EXIST = 'N/A'; export const LOCAL_STORAGE_SEARCH_QUERY = 'search-query'; -export const PAGE_PARAM = 'page'; -export const MOVIES_PER_PAGE_PARAM = 'movies-per-page'; export const DEFAULT_PAGE = 1; export const DEFAULT_MOVIES_PER_PAGE = 10; export const APP_TITLE = 'Cinemania | Dive into Movie Wonderland'; export const LOADING_STATE = 'loading'; export const SCROLL_TOP_DURATION = 300; +export const QUERY_PARAMS_INIT = { + [urlParams.PAGE]: String(DEFAULT_PAGE), + [urlParams.MOVIES_PER_PAGE]: String(DEFAULT_MOVIES_PER_PAGE), +}; diff --git a/src/shared/hooks/useUrl.ts b/src/shared/hooks/useUrl.ts index 289e793..7d123d7 100644 --- a/src/shared/hooks/useUrl.ts +++ b/src/shared/hooks/useUrl.ts @@ -2,26 +2,46 @@ import { useCallback } from 'react'; import { useSearchParams } from 'react-router-dom'; -/* -TODO - add default params -{ - [PAGE_PARAM]: String(DEFAULT_PAGE), - [MOVIES_PER_PAGE_PARAM]: String(DEFAULT_MOVIES_PER_PAGE), -} - */ +import { QUERY_PARAMS_INIT } from '../const/const.ts'; +import { UrlParams } from '../types/types.ts'; + +type SetUrl = { + (query: UrlParams, value: string | number): void; + (multipleQueriesAndValues: Record): void; // used for multiple queries at once (to avoid multiple renders and to update history only once for proper history navigation) +}; + +type ReadUrl = (query: UrlParams) => string; +/** + * Returns an object with two methods, `readUrl` and `setUrl`, that can be used to read and set URL parameters. + * + * @return obj + * @return {ReadUrl} obj.readUrl - A function that takes a query parameter and returns its value from the URL. + * @return {SetUrl} obj.setUrl - A function that takes a query parameter and its value and sets it in the URL. + */ function useUrl() { - const [searchParams, setSearchParams] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(QUERY_PARAMS_INIT); - const readUrl = useCallback( - (query: string) => - searchParams.get(query) as TQuery | null, + const readUrl: ReadUrl = useCallback( + (query: UrlParams) => searchParams.get(query) as string, [searchParams], ); - const setUrl = useCallback( - (query: string, value: string) => { - searchParams.set(query, value); + const setUrl: SetUrl = useCallback( + ( + query: UrlParams | Record, + value?: string | number, + ) => { + if (typeof query === 'object') { + Object.entries(query).forEach(([queryKey, queryValue]) => + searchParams.set(queryKey, String(queryValue)), + ); + } + + if (typeof query !== 'object' && value) { + searchParams.set(query, String(value)); + } + setSearchParams(searchParams); }, [searchParams, setSearchParams], diff --git a/src/shared/types/enums.ts b/src/shared/types/enums.ts index f7d67af..5573a79 100644 --- a/src/shared/types/enums.ts +++ b/src/shared/types/enums.ts @@ -1,7 +1,10 @@ -const itemsPerPage = { +export const itemsPerPage = { THREE: 3, FIVE: 5, TEN: 10, } as const; -export default itemsPerPage; +export const urlParams = { + PAGE: 'page', + MOVIES_PER_PAGE: 'movies-per-page', +} as const; diff --git a/src/shared/types/types.ts b/src/shared/types/types.ts index f42e410..eb19619 100644 --- a/src/shared/types/types.ts +++ b/src/shared/types/types.ts @@ -1,4 +1,4 @@ -import itemsPerPage from './enums.ts'; +import { itemsPerPage, urlParams } from './enums.ts'; export type Movie = Readonly<{ Poster: string; @@ -55,3 +55,4 @@ export type ApiMovieResponse = Readonly<{ }>; export type ItemsPerPage = (typeof itemsPerPage)[keyof typeof itemsPerPage]; +export type UrlParams = (typeof urlParams)[keyof typeof urlParams]; From 11bf7f76a7824e9cd164b7ad3fcf74025c625044 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 12:43:37 +0200 Subject: [PATCH 38/69] refactor: add type to router constant --- src/app/router.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/router.tsx b/src/app/router.tsx index 6f4b84e..97cbf59 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -1,11 +1,11 @@ -import { createHashRouter } from 'react-router-dom'; +import { createHashRouter, RouteObject } from 'react-router-dom'; import loader from '../entities/movie/loader.ts'; import AppLayout from '../pages/AppLayout/AppLayout.tsx'; import MovieDetails from '../widgets/MovieDetails/MovieDetails.tsx'; import BackButton from '../widgets/MovieDetails/ui/BackButton.tsx'; -export const ROUTES = [ +export const ROUTES: RouteObject[] = [ { element: , path: '/', From 8b6e2705796fcc1aea66587ad18707feaba2133c Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 12:49:45 +0200 Subject: [PATCH 39/69] refactor: get rid of wasted renders --- src/widgets/MovieDetails/MovieDetails.tsx | 4 ++-- src/widgets/MovieDetails/ui/Actors.tsx | 6 +++--- src/widgets/MovieDetails/ui/Description.tsx | 11 +++++++++++ src/widgets/MovieDetails/ui/Desctiption.tsx | 11 ----------- src/widgets/MovieDetails/ui/Director.tsx | 6 +++--- src/widgets/MovieDetails/ui/Genre.tsx | 6 +++--- src/widgets/MovieDetails/ui/Poster.tsx | 6 ++++-- src/widgets/MovieDetails/ui/Rating.tsx | 6 ++++-- src/widgets/MovieDetails/ui/Runtime.tsx | 6 ++++-- src/widgets/MovieDetails/ui/Title.tsx | 6 +++--- 10 files changed, 37 insertions(+), 31 deletions(-) create mode 100644 src/widgets/MovieDetails/ui/Description.tsx delete mode 100644 src/widgets/MovieDetails/ui/Desctiption.tsx diff --git a/src/widgets/MovieDetails/MovieDetails.tsx b/src/widgets/MovieDetails/MovieDetails.tsx index 68ca314..4af0e98 100644 --- a/src/widgets/MovieDetails/MovieDetails.tsx +++ b/src/widgets/MovieDetails/MovieDetails.tsx @@ -2,7 +2,7 @@ import { PropsWithChildren } from 'react'; import useMovie from './hooks/useMovie.ts'; import Actors from './ui/Actors.tsx'; -import Desctiption from './ui/Desctiption.tsx'; +import Description from './ui/Description.tsx'; import Director from './ui/Director.tsx'; import Genre from './ui/Genre.tsx'; import Poster from './ui/Poster.tsx'; @@ -39,7 +39,7 @@ function MovieDetails({ children }: PropsWithChildren) { {genre} - {description} + {description} {director} {actors} diff --git a/src/widgets/MovieDetails/ui/Actors.tsx b/src/widgets/MovieDetails/ui/Actors.tsx index 0016e2d..7417c6e 100644 --- a/src/widgets/MovieDetails/ui/Actors.tsx +++ b/src/widgets/MovieDetails/ui/Actors.tsx @@ -1,12 +1,12 @@ -import { PropsWithChildren } from 'react'; +import { memo, PropsWithChildren } from 'react'; -function Actors({ children }: PropsWithChildren) { +const Actors = memo(function Actors({ children }: PropsWithChildren) { return (

Cast: {children}

); -} +}); export default Actors; diff --git a/src/widgets/MovieDetails/ui/Description.tsx b/src/widgets/MovieDetails/ui/Description.tsx new file mode 100644 index 0000000..54ce070 --- /dev/null +++ b/src/widgets/MovieDetails/ui/Description.tsx @@ -0,0 +1,11 @@ +import { memo, PropsWithChildren } from 'react'; + +const Description = memo(function Description({ children }: PropsWithChildren) { + return ( +

+ {children} +

+ ); +}); + +export default Description; diff --git a/src/widgets/MovieDetails/ui/Desctiption.tsx b/src/widgets/MovieDetails/ui/Desctiption.tsx deleted file mode 100644 index f8d0f34..0000000 --- a/src/widgets/MovieDetails/ui/Desctiption.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { PropsWithChildren } from 'react'; - -function Desctiption({ children }: PropsWithChildren) { - return ( -

- {children} -

- ); -} - -export default Desctiption; diff --git a/src/widgets/MovieDetails/ui/Director.tsx b/src/widgets/MovieDetails/ui/Director.tsx index e6bf138..c024ec6 100644 --- a/src/widgets/MovieDetails/ui/Director.tsx +++ b/src/widgets/MovieDetails/ui/Director.tsx @@ -1,12 +1,12 @@ -import { PropsWithChildren } from 'react'; +import { memo, PropsWithChildren } from 'react'; -function Director({ children }: PropsWithChildren) { +const Director = memo(function Director({ children }: PropsWithChildren) { return (

Directed By: {children}

); -} +}); export default Director; diff --git a/src/widgets/MovieDetails/ui/Genre.tsx b/src/widgets/MovieDetails/ui/Genre.tsx index 21017b2..6e0488d 100644 --- a/src/widgets/MovieDetails/ui/Genre.tsx +++ b/src/widgets/MovieDetails/ui/Genre.tsx @@ -1,7 +1,7 @@ -import { PropsWithChildren } from 'react'; +import { memo, PropsWithChildren } from 'react'; -function Genre({ children }: PropsWithChildren) { +const Genre = memo(function Genre({ children }: PropsWithChildren) { return {children}; -} +}); export default Genre; diff --git a/src/widgets/MovieDetails/ui/Poster.tsx b/src/widgets/MovieDetails/ui/Poster.tsx index b6b2d3a..5055949 100644 --- a/src/widgets/MovieDetails/ui/Poster.tsx +++ b/src/widgets/MovieDetails/ui/Poster.tsx @@ -1,9 +1,11 @@ +import { memo } from 'react'; + interface IMoviePosterProps { poster: string; title: string; } -function Poster({ poster, title }: IMoviePosterProps) { +const Poster = memo(function Poster({ poster, title }: IMoviePosterProps) { return ( {`The ); -} +}); export default Poster; diff --git a/src/widgets/MovieDetails/ui/Rating.tsx b/src/widgets/MovieDetails/ui/Rating.tsx index 1f18af9..fe9a788 100644 --- a/src/widgets/MovieDetails/ui/Rating.tsx +++ b/src/widgets/MovieDetails/ui/Rating.tsx @@ -1,14 +1,16 @@ +import { memo } from 'react'; + interface IMovieRating { rating: string; votes: string; } -function Rating({ rating, votes }: IMovieRating) { +const Rating = memo(function Rating({ rating, votes }: IMovieRating) { return ( ⭐{rating}10 | 🍿{votes} ); -} +}); export default Rating; diff --git a/src/widgets/MovieDetails/ui/Runtime.tsx b/src/widgets/MovieDetails/ui/Runtime.tsx index 9c91170..ed3b83b 100644 --- a/src/widgets/MovieDetails/ui/Runtime.tsx +++ b/src/widgets/MovieDetails/ui/Runtime.tsx @@ -1,14 +1,16 @@ +import { memo } from 'react'; + interface IMovieRuntime { time: string; year: string; } -function Runtime({ time, year }: IMovieRuntime) { +const Runtime = memo(function Runtime({ time, year }: IMovieRuntime) { return ( {time} | {year} ); -} +}); export default Runtime; diff --git a/src/widgets/MovieDetails/ui/Title.tsx b/src/widgets/MovieDetails/ui/Title.tsx index 6701cb8..9b26e28 100644 --- a/src/widgets/MovieDetails/ui/Title.tsx +++ b/src/widgets/MovieDetails/ui/Title.tsx @@ -1,6 +1,6 @@ -import { PropsWithChildren } from 'react'; +import { memo, PropsWithChildren } from 'react'; -function Title({ children }: PropsWithChildren) { +const Title = memo(function Title({ children }: PropsWithChildren) { return (

); -} +}); export default Title; From 05ba028b299bcea6206876f3ee96d498da7d3628 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 12:52:09 +0200 Subject: [PATCH 40/69] feat: add JSDoc comment to custom hook --- src/features/Search/Search.tsx | 2 +- src/shared/hooks/useLocalStorageState.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 7dbb656..4ac0803 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -36,7 +36,7 @@ function Search({ scroll }: IMovieListProps) { const handleSearch = useCallback( async (newQuery: string) => { - setUrl(urlParams.PAGE, String(DEFAULT_PAGE)); + setUrl(urlParams.PAGE, DEFAULT_PAGE); fetchMovies(newQuery.trim()); scroll?.current?.scrollTo('top', { duration: SCROLL_TOP_DURATION }); }, diff --git a/src/shared/hooks/useLocalStorageState.ts b/src/shared/hooks/useLocalStorageState.ts index ba84ed6..ad85956 100644 --- a/src/shared/hooks/useLocalStorageState.ts +++ b/src/shared/hooks/useLocalStorageState.ts @@ -1,5 +1,12 @@ import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +/** + * Hook to create a state that persists in the local storage. + * + * @param {string} initialState - The initial state value. + * @param {string} key - The key to store the state in the local storage. + * @return {[string, Dispatch>]} - An array containing the state value and a function to update the state. + */ function useLocalStorageState(initialState: string, key: string) { const storedValue = localStorage.getItem(key); const init = storedValue || initialState; From 914a013dcbf213d95fa94a6bef2af4b4d2003394 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 13:06:11 +0200 Subject: [PATCH 41/69] feat: add lazy import --- src/app/router.tsx | 8 +------- src/widgets/MovieDetails/MovieDetails.tsx | 10 ++++++---- src/widgets/MovieDetails/ui/BackButton.tsx | 6 ++++-- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/app/router.tsx b/src/app/router.tsx index 97cbf59..956865e 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -2,8 +2,6 @@ import { createHashRouter, RouteObject } from 'react-router-dom'; import loader from '../entities/movie/loader.ts'; import AppLayout from '../pages/AppLayout/AppLayout.tsx'; -import MovieDetails from '../widgets/MovieDetails/MovieDetails.tsx'; -import BackButton from '../widgets/MovieDetails/ui/BackButton.tsx'; export const ROUTES: RouteObject[] = [ { @@ -11,11 +9,7 @@ export const ROUTES: RouteObject[] = [ path: '/', children: [ { - element: ( - - - - ), + lazy: () => import('../widgets/MovieDetails/MovieDetails.tsx'), path: ':movieId', loader, }, diff --git a/src/widgets/MovieDetails/MovieDetails.tsx b/src/widgets/MovieDetails/MovieDetails.tsx index 4af0e98..5e19fbf 100644 --- a/src/widgets/MovieDetails/MovieDetails.tsx +++ b/src/widgets/MovieDetails/MovieDetails.tsx @@ -1,7 +1,9 @@ -import { PropsWithChildren } from 'react'; +// ⬇️ needs to be disable in order lazy route to work +/* eslint-disable import/prefer-default-export */ import useMovie from './hooks/useMovie.ts'; import Actors from './ui/Actors.tsx'; +import BackButton from './ui/BackButton.tsx'; import Description from './ui/Description.tsx'; import Director from './ui/Director.tsx'; import Genre from './ui/Genre.tsx'; @@ -10,7 +12,7 @@ import Rating from './ui/Rating.tsx'; import Runtime from './ui/Runtime.tsx'; import Title from './ui/Title.tsx'; -function MovieDetails({ children }: PropsWithChildren) { +export function Component() { const { description, imdbRating, @@ -34,7 +36,7 @@ function MovieDetails({ children }: PropsWithChildren) {
- {children} + {title} {genre} @@ -48,4 +50,4 @@ function MovieDetails({ children }: PropsWithChildren) { ); } -export default MovieDetails; +Component.displayName = 'MovieDetails'; diff --git a/src/widgets/MovieDetails/ui/BackButton.tsx b/src/widgets/MovieDetails/ui/BackButton.tsx index d4473b6..e862fd6 100644 --- a/src/widgets/MovieDetails/ui/BackButton.tsx +++ b/src/widgets/MovieDetails/ui/BackButton.tsx @@ -1,7 +1,9 @@ +import { memo } from 'react'; + import chevronLeft from '../../../assets/chevron-left.svg'; import LinkWithQuery from '../../../shared/ui/LinkWithQuery.tsx'; -function BackButton() { +const BackButton = memo(function BackButton() { return (
@@ -18,6 +20,6 @@ function BackButton() {
); -} +}); export default BackButton; From 0ea38b6da89568d9d97924183c5d9a9756ef887d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 13:10:25 +0200 Subject: [PATCH 42/69] feat: add JSDoc comment to custom hook --- src/features/MovieList/hooks/useListClick.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/features/MovieList/hooks/useListClick.ts b/src/features/MovieList/hooks/useListClick.ts index 24caf41..5d9bb09 100644 --- a/src/features/MovieList/hooks/useListClick.ts +++ b/src/features/MovieList/hooks/useListClick.ts @@ -10,6 +10,14 @@ import { import useUrl from '../../../shared/hooks/useUrl.ts'; import { urlParams } from '../../../shared/types/enums.ts'; +/** + * Closes the details section on list click. + * + * @param {RefObject} scroll - A reference to the scroll container. Used to scroll to the top of the page + * @return obj - An object containing the list reference and the click handler. + * @return {RefObject} obj.listRef - A reference to the list element. + * @return {(e: MouseEvent) => void} obj.handleClick - A function to handle the click event. + * */ function useListClick(scroll: RefObject) { const listRef = useRef(null); const navigate = useNavigate(); From 4dd5a804d55f4c96288e72f9f2b2c3f1b6a3dc03 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 15:22:53 +0200 Subject: [PATCH 43/69] fix: to not fetch on same query --- src/features/Search/Search.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 4ac0803..d0c3158 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -28,7 +28,7 @@ function Search({ scroll }: IMovieListProps) { LOCAL_STORAGE_SEARCH_QUERY, ); const inputRef = useRef(null); - const { fetchMovies } = useSearch(); + const { fetchMovies, query: currQuery, updateQuery } = useSearch(); const { setUrl } = useUrl(); useKey(ENTER_KEY, handleEnter); @@ -36,11 +36,15 @@ function Search({ scroll }: IMovieListProps) { const handleSearch = useCallback( async (newQuery: string) => { + if (newQuery === currQuery) return; + setUrl(urlParams.PAGE, DEFAULT_PAGE); - fetchMovies(newQuery.trim()); scroll?.current?.scrollTo('top', { duration: SCROLL_TOP_DURATION }); + + fetchMovies(newQuery.trim()); + updateQuery(query); }, - [fetchMovies, scroll, setUrl], + [currQuery, fetchMovies, query, scroll, setUrl, updateQuery], ); function handleEnter() { From 8a4823b0282f98ba0b7e6dc2fdab9388a7a4cdc9 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 15:24:39 +0200 Subject: [PATCH 44/69] fix: eslint error --- src/features/Search/Search.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index d0c3158..00ce364 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ - import { RefObject, useCallback, useRef } from 'react'; import LocomotiveScroll from 'locomotive-scroll'; @@ -31,9 +29,6 @@ function Search({ scroll }: IMovieListProps) { const { fetchMovies, query: currQuery, updateQuery } = useSearch(); const { setUrl } = useUrl(); - useKey(ENTER_KEY, handleEnter); - useKey(ESCAPE_KEY, handleEscape); - const handleSearch = useCallback( async (newQuery: string) => { if (newQuery === currQuery) return; @@ -71,6 +66,9 @@ function Search({ scroll }: IMovieListProps) { } } + useKey(ENTER_KEY, handleEnter); + useKey(ESCAPE_KEY, handleEscape); + return (
Date: Fri, 10 Nov 2023 15:27:05 +0200 Subject: [PATCH 45/69] refactor: change tooltip animation timing --- src/shared/ui/Tooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/ui/Tooltip.tsx b/src/shared/ui/Tooltip.tsx index 88e9bdb..13c59a8 100644 --- a/src/shared/ui/Tooltip.tsx +++ b/src/shared/ui/Tooltip.tsx @@ -11,7 +11,7 @@ function Tooltip({ innerRef, children }: ITooltipProps) { return createPortal(
+ className="pointer-events-none invisible absolute left-0 top-0 flex h-28 w-28 items-center justify-center rounded-full bg-lime-400 p-6 text-center text-sm font-bold text-zinc-950 opacity-0 [transition:_translate_1500ms_cubic-bezier(.08,.9,.21,.98),_transform_1000ms_cubic-bezier(.13,.66,0,.95),_opacity_250ms,_visibility_250ms]"> {children}
, document.body, From fdd8b4a06dbf5c886743e7c4cf2e2f63b5c47715 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 19:30:19 +0200 Subject: [PATCH 46/69] feat: add radial hover fade in animation --- src/entities/movie/ui/Movie.tsx | 2 +- src/shared/lib/helpers/animateRadialHover.ts | 47 +++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 281f4dc..3633ed3 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -41,7 +41,7 @@ const Movie = memo(function Movie({ handleMouseMove(e); onMouseMove(e); }} - onMouseOut={() => { + onMouseLeave={() => { handleMouseOut(); onMouseOut(); }} diff --git a/src/shared/lib/helpers/animateRadialHover.ts b/src/shared/lib/helpers/animateRadialHover.ts index 2228d38..4f0bfce 100644 --- a/src/shared/lib/helpers/animateRadialHover.ts +++ b/src/shared/lib/helpers/animateRadialHover.ts @@ -10,14 +10,59 @@ type CleanUpFn = (elem: HTMLElement) => void; type RadialHover = [AnimationFn, CleanUpFn]; +const THRESHOLD = 700; +const ANIMATION_DURATION = 450; +let isEnd = false; +let animationFrameId: number; +let pointerX = 0; +let pointerY = 0; + +function animate( + currTimeStamp: number, + startTimeStamp: number, + elem: HTMLElement, +) { + const animationProgress = + (currTimeStamp - startTimeStamp) / ANIMATION_DURATION; + const value = Math.sin((animationProgress * Math.PI) / 2); + + if (animationProgress < 1) { + elem.style.background = `radial-gradient(circle at ${pointerX}px ${pointerY}px, rgb(112, 26, 117, 0.5) 0%, ${ + colors.transparent + } ${value * THRESHOLD}px)`; + animationFrameId = requestAnimationFrame((t) => + animate(t, startTimeStamp, elem), + ); + } + + if (animationProgress >= 1) { + elem.style.background = `radial-gradient(circle at ${pointerX}px ${pointerY}px, rgb(112, 26, 117, 0.5) 0%, ${colors.transparent} ${THRESHOLD}px)`; + isEnd = true; + } +} + function animateRadialHover(elem: HTMLElement, e: MouseEvent) { const { posX, posY } = getElementMouseCoord(elem, e); + const startTimeStamp = performance.now(); + pointerX = posX; + pointerY = posY; + + if (isEnd) { + elem.style.background = `radial-gradient(circle at ${posX}px ${posY}px, rgb(112, 26, 117, 0.5) 0%, ${colors.transparent} ${THRESHOLD}px)`; + return; + } - elem.style.background = `radial-gradient(circle at ${posX}px ${posY}px, rgb(112, 26, 117, 0.5) 0%, ${colors.transparent} 160px)`; + if (!animationFrameId) + animationFrameId = requestAnimationFrame(() => + animate(startTimeStamp, startTimeStamp, elem), + ); } function cleanUp(elem: HTMLElement) { + cancelAnimationFrame(animationFrameId); + animationFrameId = 0; elem.style.background = ''; + isEnd = false; } function createRadialHover(): RadialHover { From f66d2f29090b7f4438b65273a77095c13f7829cc Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 23:14:52 +0200 Subject: [PATCH 47/69] refactor: change test cases naming --- src/entities/movie/Movie.test.tsx | 6 +++--- src/features/MovieList/MovieList.test.tsx | 18 ++++++++++++++---- src/pages/AppLayout/AppLayout.test.tsx | 2 +- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/entities/movie/Movie.test.tsx b/src/entities/movie/Movie.test.tsx index 97501f7..87a2dd8 100644 --- a/src/entities/movie/Movie.test.tsx +++ b/src/entities/movie/Movie.test.tsx @@ -18,7 +18,7 @@ describe('Movie', () => { vi.clearAllMocks(); }); - it('Movie component renders the relevant movie data', () => { + it('should render the relevant movie data', () => { renderWithRouter( { expect(year).toHaveTextContent(mockMovie.Year); }); - it('Clicking on a card opens a detailed card component', async () => { + it('should open a detailed card component when clicking on a card', async () => { mockedUseSearch.mockReturnValue(createMockSearchContext()); renderWithRouter(); @@ -55,7 +55,7 @@ describe('Movie', () => { expect(detailsSection).toBeInTheDocument(); }); - it('Clicking on the card triggers an additional API call to fetch detailed information', async () => { + it('should triggers an additional API call to fetch detailed information when clicking on the card', async () => { mockedUseSearch.mockReturnValue(createMockSearchContext()); renderWithRouter(); diff --git a/src/features/MovieList/MovieList.test.tsx b/src/features/MovieList/MovieList.test.tsx index fc0048c..915b085 100644 --- a/src/features/MovieList/MovieList.test.tsx +++ b/src/features/MovieList/MovieList.test.tsx @@ -1,6 +1,8 @@ +import { RefObject } from 'react'; + import { screen } from '@testing-library/react'; import LocomotiveScroll from 'locomotive-scroll'; -import { describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import MovieList from './MovieList.tsx'; import Movie from '../../entities/movie/ui/Movie.tsx'; @@ -9,10 +11,18 @@ import renderWithRouter from '../../test/helpers/RenderWithRouter.tsx'; import * as useSearch from '../Search/hooks/useSearch.ts'; const mockedUseSearch = vi.spyOn(useSearch, 'default'); -const scroll = { current: new LocomotiveScroll() }; +let scroll: RefObject; describe('MovieList', () => { - it('Creates an empty movie list', () => { + beforeAll(() => { + scroll = { current: new LocomotiveScroll() }; + }); + + afterAll(() => { + scroll.current?.destroy(); + }); + + it('should create an empty movie list', () => { mockedUseSearch.mockReturnValue(createMockSearchContext(null)); renderWithRouter( @@ -35,7 +45,7 @@ describe('MovieList', () => { ).toBeInTheDocument(); }); - it('Creates the movie list with movies', () => { + it('should create the movie list with movies', () => { mockedUseSearch.mockReturnValue(createMockSearchContext()); renderWithRouter( diff --git a/src/pages/AppLayout/AppLayout.test.tsx b/src/pages/AppLayout/AppLayout.test.tsx index 02b313b..ae1b5a5 100644 --- a/src/pages/AppLayout/AppLayout.test.tsx +++ b/src/pages/AppLayout/AppLayout.test.tsx @@ -4,7 +4,7 @@ import { describe, it } from 'vitest'; import renderWithRouter from '../../test/helpers/RenderWithRouter.tsx'; describe('App', () => { - it('Renders the app', async () => { + it('should render the app', async () => { // 1. arrange renderWithRouter(); // 2. act From 6bf1f6f00b5be5d073cde03e5aaa5cf441341ca1 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 10 Nov 2023 23:16:01 +0200 Subject: [PATCH 48/69] fix: test case --- src/test/mocks/handlers.ts | 6 ++++-- src/widgets/MovieDetails/MovieDetails.test.tsx | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/test/mocks/handlers.ts b/src/test/mocks/handlers.ts index 1f7907a..d550642 100644 --- a/src/test/mocks/handlers.ts +++ b/src/test/mocks/handlers.ts @@ -1,11 +1,13 @@ -import { http, HttpResponse } from 'msw'; +import { delay, http, HttpResponse } from 'msw'; import { mockMovieDetails, mockMovieDetailsNoPoster } from './data.ts'; import { API_URL_NO_KEY } from '../../shared/const/const.ts'; import NO_POSTER_QUERY_TEST_CASE from '../const/const.ts'; const handlers = [ - http.get(`${API_URL_NO_KEY}`, ({ request }) => { + http.get(`${API_URL_NO_KEY}`, async ({ request }) => { + await delay(); + const url = new URL(request.url); const movieId = url.searchParams.get('i'); diff --git a/src/widgets/MovieDetails/MovieDetails.test.tsx b/src/widgets/MovieDetails/MovieDetails.test.tsx index 3f2d983..ae402f7 100644 --- a/src/widgets/MovieDetails/MovieDetails.test.tsx +++ b/src/widgets/MovieDetails/MovieDetails.test.tsx @@ -20,10 +20,11 @@ describe('Movie details', () => { }); it('should display the loader while fetching data', async () => { - renderWithRouter(null, ['/test']); + renderWithRouter(null, ['/delay']); const loader = await screen.findByTestId('loader'); - expect(loader).toBeInTheDocument(); + + expect(loader).toBeDefined(); }); it('should properly calculate and edit the runtime', async () => { From be9193e9fe4e3a6d1da9da99b75ccf87c0eeca2d Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 11 Nov 2023 18:04:58 +0200 Subject: [PATCH 49/69] feat: add view transition animations --- src/entities/movie/ui/Movie.tsx | 10 +++++++++- src/features/MovieList/ui/PageNum.tsx | 6 +++++- src/shared/ui/LinkWithQuery.tsx | 15 +++++++++++++-- src/shared/ui/Tabs.tsx | 6 +++++- src/widgets/Header/Header.tsx | 3 +++ src/widgets/MovieDetails/ui/Rating.tsx | 2 +- 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/entities/movie/ui/Movie.tsx b/src/entities/movie/ui/Movie.tsx index 3633ed3..404b666 100644 --- a/src/entities/movie/ui/Movie.tsx +++ b/src/entities/movie/ui/Movie.tsx @@ -1,5 +1,7 @@ import { memo, MouseEvent } from 'react'; +import { useLocation } from 'react-router-dom'; + import ReactLogo from '../../../assets/reactJS-logo.png'; import { NOT_EXIST } from '../../../shared/const/const.ts'; import useRadialHover from '../../../shared/hooks/useRadialHover.ts'; @@ -21,18 +23,24 @@ const Movie = memo(function Movie({ }: IMovieProps) { const { handleMouseOut, handleMouseMove, containerRef } = useRadialHover(); + const { pathname } = useLocation(); const { Poster, Title, Year, imdbID } = data; const poster = Poster === NOT_EXIST ? ReactLogo : Poster; const animationDelay = `0.${String(delay)}s`; + const isDetailsClose = pathname.slice(1) === ''; return ( - +
  • + {currPage} of {maxPage} ); diff --git a/src/shared/ui/LinkWithQuery.tsx b/src/shared/ui/LinkWithQuery.tsx index 9323e3c..1838eac 100644 --- a/src/shared/ui/LinkWithQuery.tsx +++ b/src/shared/ui/LinkWithQuery.tsx @@ -1,23 +1,34 @@ import { ReactNode } from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import { + Link, + useLocation, + unstable_useViewTransitionState as useViewTransitionState, +} from 'react-router-dom'; interface ILinkWithQueryProps { children: ReactNode; to: string; className?: string; + viewTransition?: boolean; } function LinkWithQuery({ children, className = '', to, + viewTransition = true, ...props }: ILinkWithQueryProps) { const { search } = useLocation(); + useViewTransitionState(to + search); return ( - + {children} ); diff --git a/src/shared/ui/Tabs.tsx b/src/shared/ui/Tabs.tsx index c798c51..2346726 100644 --- a/src/shared/ui/Tabs.tsx +++ b/src/shared/ui/Tabs.tsx @@ -47,7 +47,11 @@ function Tabs({ ); return ( -
    +
    {children} diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx index 02f4062..98827d7 100644 --- a/src/widgets/Header/Header.tsx +++ b/src/widgets/Header/Header.tsx @@ -6,6 +6,9 @@ function Header({ children }: IChildren) { data-scroll="true" data-scroll-sticky="true" data-scroll-target="section" + style={{ + viewTransitionName: 'header', + }} className="sticky top-0 z-10 flex w-full flex-wrap items-center justify-between gap-x-6 gap-y-4 p-6 backdrop-blur-xl sm:gap-0 lg:h-24 lg:px-12 xl:px-20"> {children}
  • diff --git a/src/widgets/MovieDetails/ui/Rating.tsx b/src/widgets/MovieDetails/ui/Rating.tsx index fe9a788..633a026 100644 --- a/src/widgets/MovieDetails/ui/Rating.tsx +++ b/src/widgets/MovieDetails/ui/Rating.tsx @@ -8,7 +8,7 @@ interface IMovieRating { const Rating = memo(function Rating({ rating, votes }: IMovieRating) { return ( - ⭐{rating}10 | 🍿{votes} + ⭐{rating}/10 | 🍿{votes} ); }); From 4bf234ba18ae6e2656a9817ccdee649a17c4eaac Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 11 Nov 2023 21:38:06 +0200 Subject: [PATCH 50/69] feat: add pagination tests --- src/features/Pagination/Pagination.test.tsx | 46 +++++++++++++++++++++ src/features/Pagination/ui/Button.tsx | 1 + 2 files changed, 47 insertions(+) create mode 100644 src/features/Pagination/Pagination.test.tsx diff --git a/src/features/Pagination/Pagination.test.tsx b/src/features/Pagination/Pagination.test.tsx new file mode 100644 index 0000000..9bdc66a --- /dev/null +++ b/src/features/Pagination/Pagination.test.tsx @@ -0,0 +1,46 @@ +import { RefObject } from 'react'; + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import LocomotiveScroll from 'locomotive-scroll'; +import { MemoryRouter } from 'react-router-dom'; +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; + +import Pagination from './Pagination.tsx'; +import * as useUrl from '../../shared/hooks/useUrl.ts'; + +const mockedUseUrl = vi.spyOn(useUrl, 'default'); + +let scroll: RefObject; + +describe('Pagination', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + beforeAll(() => { + scroll = { current: new LocomotiveScroll() }; + }); + + afterAll(() => { + scroll.current?.destroy(); + }); + + it('should update URL query parameter when page changes', async () => { + render( + + + , + ); + + const [button] = screen.getAllByTestId('pagination'); + + expect(button).toBeInTheDocument(); + + await userEvent.click(button); + await userEvent.click(button); + await userEvent.click(button); + + expect(mockedUseUrl).toHaveBeenCalled(); + }); +}); diff --git a/src/features/Pagination/ui/Button.tsx b/src/features/Pagination/ui/Button.tsx index 897f535..889219e 100644 --- a/src/features/Pagination/ui/Button.tsx +++ b/src/features/Pagination/ui/Button.tsx @@ -24,6 +24,7 @@ const Button = memo(function Button({ return (