-
-
-
-
-
+
);
}
diff --git a/src/pages/NotFound/NotFound.tsx b/src/pages/NotFound/NotFound.tsx
index 4fefb5ce..60dd4dfa 100644
--- a/src/pages/NotFound/NotFound.tsx
+++ b/src/pages/NotFound/NotFound.tsx
@@ -1,6 +1,6 @@
import LinkWithQuery from '../../shared/ui/LinkWithQuery.tsx';
import Modal from '../../shared/ui/Modal.tsx';
-import GradientBackground from '../AppLayout/ui/GradientBackground.tsx';
+import GradientBackground from "../AppLayout/ui/GradientBackground.tsx";
function NotFound() {
return (
diff --git a/src/shared/api/baseQuery.ts b/src/shared/api/baseQuery.ts
new file mode 100644
index 00000000..c28942c3
--- /dev/null
+++ b/src/shared/api/baseQuery.ts
@@ -0,0 +1,7 @@
+import { fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react';
+
+const baseQuery = fetchBaseQuery({
+ baseUrl: `${import.meta.env.VITE_API_URL}`,
+});
+
+export default baseQuery;
diff --git a/src/shared/api/rootApi.ts b/src/shared/api/rootApi.ts
new file mode 100644
index 00000000..ad89ec35
--- /dev/null
+++ b/src/shared/api/rootApi.ts
@@ -0,0 +1,11 @@
+import { createApi } from '@reduxjs/toolkit/query/react';
+
+import baseQuery from './baseQuery.ts';
+
+const rootApi = createApi({
+ reducerPath: 'rootApi',
+ baseQuery,
+ endpoints: () => ({}),
+});
+
+export default rootApi;
diff --git a/src/shared/const/const.ts b/src/shared/const/const.ts
index d3da5127..96f6c510 100644
--- a/src/shared/const/const.ts
+++ b/src/shared/const/const.ts
@@ -1,17 +1,13 @@
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 DEFAULT_PAGE = 1;
-export const DEFAULT_MOVIES_PER_PAGE = 10;
+export const 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),
+ [urlParams.MOVIES_PER_PAGE]: String(MOVIES_PER_PAGE),
};
diff --git a/src/shared/hooks/useAnime.ts b/src/shared/hooks/useAnime.ts
new file mode 100644
index 00000000..9ee074fb
--- /dev/null
+++ b/src/shared/hooks/useAnime.ts
@@ -0,0 +1,42 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+
+import { DependencyList, useEffect, useRef } from 'react';
+
+import { AnimeInstance } from 'animejs';
+import anime from 'animejs/lib/anime.es.js';
+
+import reduceAnimeTargets from '../lib/helpers/reduceAnimeTargets.ts';
+import { IParams } from '../types/interfaces.ts';
+import { AnimeTarget } from '../types/types.ts';
+
+/**
+ * Applies anime.js animation to the element using the specified parameters and dependencies.
+ *
+ * @template TElem - The type of the HTML element to animate.
+ * @param {AnimeParams} params - The parameters used to configure the animation.
+ * @param {DependencyList} [deps=[]] - The list of dependencies that trigger the animation when changed.
+ * @param isChildren
+ * @return {React.MutableRefObject
} The reference to the HTML element being animated.
+ * You can also pass the targets param to params object, if your component already have ref to an element that needs to be animated
+ */
+function useAnime(
+ params: IParams,
+ deps: DependencyList = [],
+ isChildren: boolean = false,
+) {
+ const elementRef = useRef(null);
+ const animeRef = useRef();
+
+ useEffect(() => {
+ const targets = reduceAnimeTargets(params, elementRef, isChildren);
+
+ animeRef.current = anime({
+ ...params,
+ targets,
+ });
+ }, [...deps]);
+
+ return { elementRef, animation: animeRef };
+}
+
+export default useAnime;
diff --git a/src/shared/hooks/useAnime.tsx b/src/shared/hooks/useAnime.tsx
deleted file mode 100644
index dce18b30..00000000
--- a/src/shared/hooks/useAnime.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-/* eslint-disable react-hooks/exhaustive-deps */
-
-import { DependencyList, useEffect, useRef } from 'react';
-
-import { AnimeParams } from 'animejs';
-import anime from 'animejs/lib/anime.es.js';
-
-type Params = Omit;
-
-/**
- * Applies anime.js animation to the element using the specified parameters and dependencies.
- *
- * @template TElem - The type of the HTML element to animate.
- * @param {Params} params - The parameters used to configure the animation.
- * @param {DependencyList} [deps=[]] - The list of dependencies that trigger the animation when changed.
- * @return {React.MutableRefObject} The reference to the HTML element being animated.
- */
-function useAnime(
- params: Params,
- deps: DependencyList = [],
-) {
- const elementRef = useRef(null);
-
- useEffect(() => {
- anime({
- targets: elementRef.current,
- ...params,
- });
- }, [...deps]);
-
- return elementRef;
-}
-
-export default useAnime;
diff --git a/src/shared/hooks/useAnimeTimeline.ts b/src/shared/hooks/useAnimeTimeline.ts
new file mode 100644
index 00000000..fa548327
--- /dev/null
+++ b/src/shared/hooks/useAnimeTimeline.ts
@@ -0,0 +1,56 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+
+import { DependencyList, useEffect, useRef } from 'react';
+
+import { AnimeTimelineInstance } from 'animejs';
+import anime from 'animejs/lib/anime.es';
+
+import reduceAnimeTargets from '../lib/helpers/reduceAnimeTargets.ts';
+import { IParams } from '../types/interfaces.ts';
+import { AnimeTarget } from '../types/types.ts';
+
+interface IOptions {
+ set?: { targets: AnimeTarget; values: { [key: string]: number } }[];
+}
+
+/**
+ * A hook to use Anime.js timeline functionality.
+ *
+ * @param {IOptions} options - anime options.
+ * @param {DependencyList} deps - An array of dependencies to watch for changes.
+ * @param {IParams[]} add - Additional params to add to the timeline.
+ * @returns A reference to the Anime.js timeline instance.
+ */
+function useAnimeTimeline(
+ options: IOptions = {},
+ deps: DependencyList = [],
+ ...add: IParams[]
+) {
+ const timelineRef = useRef();
+
+ useEffect(() => {
+ options.set?.forEach((opt) => {
+ const targets = reduceAnimeTargets(opt);
+ anime.set(targets, opt.values);
+ });
+
+ timelineRef.current = anime.timeline({});
+
+ add.forEach((param) => {
+ const { timelineOffset, isChildren, ...restParams } = param;
+ const targets = reduceAnimeTargets(param, null, isChildren);
+
+ timelineRef.current = timelineRef.current?.add(
+ {
+ ...restParams,
+ targets,
+ },
+ timelineOffset,
+ );
+ });
+ }, [...deps]);
+
+ return timelineRef;
+}
+
+export default useAnimeTimeline;
diff --git a/src/shared/hooks/useAppDispatch.ts b/src/shared/hooks/useAppDispatch.ts
new file mode 100644
index 00000000..7cd45114
--- /dev/null
+++ b/src/shared/hooks/useAppDispatch.ts
@@ -0,0 +1,6 @@
+import { useDispatch } from 'react-redux';
+
+import { AppDispatch } from '../../app/store/store.ts';
+
+const useAppDispatch = () => useDispatch();
+export default useAppDispatch;
diff --git a/src/shared/hooks/useAppSelector.ts b/src/shared/hooks/useAppSelector.ts
new file mode 100644
index 00000000..a96e5db5
--- /dev/null
+++ b/src/shared/hooks/useAppSelector.ts
@@ -0,0 +1,6 @@
+import { TypedUseSelectorHook, useSelector } from 'react-redux';
+
+import { RootState } from '../../app/store/store.ts';
+
+const useAppSelector: TypedUseSelectorHook = useSelector;
+export default useAppSelector;
diff --git a/src/shared/hooks/useDispatchIsFetching.ts b/src/shared/hooks/useDispatchIsFetching.ts
new file mode 100644
index 00000000..a904c19e
--- /dev/null
+++ b/src/shared/hooks/useDispatchIsFetching.ts
@@ -0,0 +1,19 @@
+import { useEffect } from 'react';
+
+import useAppDispatch from './useAppDispatch.ts';
+import { dataFetchedMainPage } from '../../app/model/slice.ts';
+
+/**
+ * Dispatches the `dataFetched` action with the given `isFetching` value.
+ *
+ * @param {boolean} isFetching - A boolean value indicating whether data is being fetched.
+ * @return {void}
+ */
+function useDispatchIsFetching(isFetching: boolean) {
+ const dispatch = useAppDispatch();
+ useEffect(() => {
+ dispatch(dataFetchedMainPage(isFetching));
+ }, [dispatch, isFetching]);
+}
+
+export default useDispatchIsFetching;
diff --git a/src/shared/hooks/useDocumentTitle.ts b/src/shared/hooks/useDocumentTitle.ts
index 60d887f9..866ee06f 100644
--- a/src/shared/hooks/useDocumentTitle.ts
+++ b/src/shared/hooks/useDocumentTitle.ts
@@ -2,6 +2,12 @@ import { useEffect } from 'react';
import { APP_TITLE } from '../const/const.ts';
+/**
+ * Updates the document title when a new title is provided.
+ *
+ * @param {string} newTitle - The new title for the document.
+ * @return {void}
+ */
function useDocumentTitle(newTitle: string) {
useEffect(() => {
if (newTitle) document.title = newTitle;
diff --git a/src/shared/hooks/useGetMovie.ts b/src/shared/hooks/useGetMovie.ts
new file mode 100644
index 00000000..1d7c59ed
--- /dev/null
+++ b/src/shared/hooks/useGetMovie.ts
@@ -0,0 +1,31 @@
+import { useEffect } from 'react';
+
+import { useLocation } from 'react-router-dom';
+
+import useAppDispatch from './useAppDispatch.ts';
+import { dataFetchedDetailsPage } from '../../app/model/slice.ts';
+import { useGetMovieQuery } from '../../entities/movie/api/movieApi.ts';
+
+/**
+ * Retrieves movie data using the useGetMovieQuery hook and dispatches the isFetching state to the useDispatchIsFetching function.
+ *
+ * @returns {Object} obj - An object containing the movie data.
+ * @returns {ApiMovieResponse} obj.movie - The movie data.
+ */
+function useGetMovie() {
+ const { pathname } = useLocation();
+
+ const id = pathname.slice(1);
+ const { data: movie, isLoading, isFetching } = useGetMovieQuery(id);
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ dispatch(dataFetchedDetailsPage(isFetching || isLoading));
+ }, [dispatch, isFetching, isLoading]);
+
+ if (movie && 'Error' in movie) throw new Error(movie.Error);
+
+ return movie;
+}
+
+export default useGetMovie;
diff --git a/src/shared/hooks/useGetMovieList.ts b/src/shared/hooks/useGetMovieList.ts
new file mode 100644
index 00000000..fce95440
--- /dev/null
+++ b/src/shared/hooks/useGetMovieList.ts
@@ -0,0 +1,44 @@
+import { useEffect } from 'react';
+
+import useAppDispatch from './useAppDispatch.ts';
+import useAppSelector from './useAppSelector.ts';
+import useUrl from './useUrl.ts';
+import { dataFetchedDetailsPage } from '../../app/model/slice.ts';
+import { useGetMovieListQuery } from '../../entities/movie/api/movieApi.ts';
+import selectMoviesPerPage from '../lib/selectors/selectMoviesPerPage.ts';
+import selectSearchQuery from '../lib/selectors/selectSearchQuery.ts';
+import { urlParams } from '../types/enums.ts';
+import { MovieList } from '../types/types.ts';
+
+/**
+ * Retrieves a list of movies using the `useGetMovieListQuery` hook.
+ *
+ * @returns The list of movies fetched using the `useGetMovieListQuery` hook.
+ */
+function useGetMovieList() {
+ const { readUrl } = useUrl();
+
+ const query = useAppSelector(selectSearchQuery);
+ const moviesPerPage = useAppSelector(selectMoviesPerPage);
+ const dispatch = useAppDispatch();
+ const page = readUrl(urlParams.PAGE);
+
+ const { data, isLoading, isFetching } = useGetMovieListQuery({
+ page,
+ query,
+ moviesPerPage,
+ });
+
+ const movieList = data?.Search?.slice(0, moviesPerPage) as
+ | MovieList
+ | undefined;
+ const totalResults = Number.parseInt(data?.totalResults ?? '0', 10);
+
+ useEffect(() => {
+ dispatch(dataFetchedDetailsPage(isFetching || isLoading));
+ }, [dispatch, isFetching, isLoading]);
+
+ return { movieList, totalResults, isInitialLoading: isLoading };
+}
+
+export default useGetMovieList;
diff --git a/src/shared/hooks/useKey.ts b/src/shared/hooks/useKey.ts
index 40d77472..4615eff0 100644
--- a/src/shared/hooks/useKey.ts
+++ b/src/shared/hooks/useKey.ts
@@ -1,5 +1,13 @@
import { useCallback, useEffect } from 'react';
+/**
+ * Registers a keyboard event listener that triggers an action when a specified key is pressed.
+ *
+ * @param {string} key - The key to listen for.
+ * @param {function} action - The action to perform when the specified key is pressed.
+ *
+ * @return {void}
+ */
function useKey(key: string, action: () => void) {
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
diff --git a/src/shared/hooks/useRadialHover.ts b/src/shared/hooks/useRadialHover.ts
index ecab46d4..f8b85572 100644
--- a/src/shared/hooks/useRadialHover.ts
+++ b/src/shared/hooks/useRadialHover.ts
@@ -2,6 +2,15 @@ import { MouseEvent, useRef } from 'react';
import createRadialHover from '../lib/helpers/animateRadialHover.ts';
+/**
+ * Attaches radial hover effect to a container element.
+ *
+ * @template TContainer - The type of the container element.
+ * @returns obj - Returns an object with event handler functions and a container reference..
+ * @returns obj.handleMouseMove: (e: MouseEvent) => void.
+ * @returns obj.handleMouseOut: () => void.
+ * @returns obj.containerRef: React.MutableRefObject.
+ */
function useRadialHover() {
const containerRef = useRef(null);
const [radialHover, cleanUp] = createRadialHover();
diff --git a/src/shared/hooks/useScroll.ts b/src/shared/hooks/useScroll.ts
index 2f70d5ac..5baf5d85 100644
--- a/src/shared/hooks/useScroll.ts
+++ b/src/shared/hooks/useScroll.ts
@@ -2,6 +2,14 @@ import { RefObject, useEffect, useRef } from 'react';
import LocomotiveScroll from 'locomotive-scroll';
+/**
+ * Hook for using Locomotive Scroll.
+ *
+ * @template TContainer - The type of the container element.
+ * @returns obj - An object containing `containerRef` and `scrollRef` as React Ref Objects.
+ * @returns obj.containerRef {TContainer} - ref with the container element that scroll is attached to.
+ * @returns obj.scrollRef {LocomotiveScroll} - ref with the locomotive scroll instance.
+ */
function useScroll() {
const containerRef = useRef(null);
const scrollRef = useRef();
diff --git a/src/shared/hooks/useScrollTop.ts b/src/shared/hooks/useScrollTop.ts
index b7476cb2..53340975 100644
--- a/src/shared/hooks/useScrollTop.ts
+++ b/src/shared/hooks/useScrollTop.ts
@@ -5,10 +5,17 @@ import LocomotiveScroll from 'locomotive-scroll';
import { SCROLL_TOP_DURATION } from '../const/const.ts';
+/**
+ * Update scroll position to top when `currValue` changes.
+ *
+ * @param {unknown} currValue - The current value.
+ * @param {RefObject | undefined} scroll - The scroll reference.
+ * @param {...DependencyList} deps - Additional dependencies.
+ * @return {void}
+ */
function useScrollTop(
currValue: unknown,
scroll: RefObject | undefined,
- callback?: () => void,
...deps: DependencyList
) {
const prevValueRef = useRef(currValue);
@@ -17,7 +24,6 @@ function useScrollTop(
if (prevValueRef.current !== currValue) {
prevValueRef.current = currValue;
scroll?.current?.scrollTo?.('top', { duration: SCROLL_TOP_DURATION });
- callback?.();
}
}, [currValue, scroll, ...deps]);
}
diff --git a/src/shared/hooks/useTabs.ts b/src/shared/hooks/useTabs.ts
new file mode 100644
index 00000000..c3f2587c
--- /dev/null
+++ b/src/shared/hooks/useTabs.ts
@@ -0,0 +1,42 @@
+import { useEffect, useRef, useState } from 'react';
+
+import { itemsPerPage } from '../types/enums.ts';
+
+/**
+ * Sets the position of a slider tab based on the active value and number of tabs.
+ * @param {number} activeValue - The active value representing the current tab.
+ * @param {number} numOfTabs - The number of tabs.
+ * @return {Object} obj An object containing the position, container reference, and tab slider width.
+ * @return {number} obj.position - The new position of the tab slider
+ * @return {RefObject} obj.containerRef - Ref with the container
+ * @return {RefObject} obj.tabSliderWidth - Ref with cached width of the tab slider
+ */
+function useTabs(activeValue: number, numOfTabs: number) {
+ const [tabSliderWidth, setTabSliderWidth] = useState(0);
+ const containerRef = useRef(null);
+ const containerWidth = useRef(0);
+ const containerPadding = useRef(6);
+
+ const halfContainer = containerWidth.current / 2;
+ const halfTabsSlider = tabSliderWidth / 2;
+
+ const start = containerPadding.current;
+ const middle = halfContainer - halfTabsSlider;
+ const end =
+ containerWidth.current - tabSliderWidth - containerPadding.current;
+
+ let position = start;
+ if (activeValue === itemsPerPage.FIVE) position = middle;
+ if (activeValue === itemsPerPage.TEN) position = end;
+
+ useEffect(() => {
+ containerWidth.current = containerRef.current?.offsetWidth ?? 0;
+ setTabSliderWidth(
+ containerWidth.current / numOfTabs - containerPadding.current,
+ );
+ }, [numOfTabs]);
+
+ return { position, containerRef, tabSliderWidth };
+}
+
+export default useTabs;
diff --git a/src/shared/hooks/useTooltip.ts b/src/shared/hooks/useTooltip.ts
index 417bc629..8d674e0f 100644
--- a/src/shared/hooks/useTooltip.ts
+++ b/src/shared/hooks/useTooltip.ts
@@ -2,38 +2,70 @@ import { RefObject, useCallback, useEffect, useRef } from 'react';
import LocomotiveScroll from 'locomotive-scroll';
+import useAnime from './useAnime.ts';
import getElementMouseCoord from '../lib/helpers/getElementMouseCoord.ts';
-const HIDDEN = ['invisible', 'opacity-0', '!scale-[.3]'];
const ELEMENT_POSITION_OFFSET = 120;
+/**
+ * Floating tooltip bubble following the mouse.
+ * The tooltip is displayed when the element is hovered and follows the mouse movement.
+ *
+ * @param {RefObject} scroll - The reference to the LocomotiveScroll instance. Used to hide the tooltip on scroll
+ * @return {Object} obj - An object containing the tooltip reference, hideTooltip function, and showTooltip function.
+ * @return {HTMLDivElement} obj.tooltipRef - The ref with the tooltip element.
+ * @return {() => void} obj.hideTooltip - Function that is used to hide the tooltip. For example on mouseLeave event.
+ * @return {() => void} obj.showTooltip - Function that is used to show the tooltip. For example on mouseEnter event.
+ */
function useTooltip(scroll: RefObject) {
const tooltipRef = useRef(null);
- const moveTooltip = useCallback((e: MouseEvent) => {
- const { posX, posY } = getElementMouseCoord(document.body, e);
+ const { animation: showTooltipAnimation } = useAnime({
+ targets: tooltipRef,
+ scale: [0, 1],
+ opacity: [0, 1],
+ translateZ: 0,
+ duration: 1200,
+ });
- const pointerX = posX - ELEMENT_POSITION_OFFSET;
- const pointerY = posY - ELEMENT_POSITION_OFFSET;
+ const { animation: hideTooltipAnimation } = useAnime({
+ targets: tooltipRef,
+ scale: [1, 0],
+ opacity: [1, 0],
+ translateZ: 0,
+ duration: 1200,
+ });
- if (tooltipRef.current)
- tooltipRef.current.style.translate = `${pointerX}px ${pointerY}px`;
- }, []);
+ const moveTooltip = useCallback(
+ (e: MouseEvent) => {
+ const { posX, posY } = getElementMouseCoord(document.body, e);
+
+ const pointerX = posX - ELEMENT_POSITION_OFFSET;
+ const pointerY = posY - ELEMENT_POSITION_OFFSET;
+
+ if (tooltipRef.current)
+ tooltipRef.current.style.translate = `${pointerX}px ${pointerY}px`;
+ },
+ [tooltipRef],
+ );
const showTooltip = useCallback(() => {
- tooltipRef.current?.classList.remove(...HIDDEN);
- }, []);
+ showTooltipAnimation.current?.restart();
+ }, [showTooltipAnimation]);
const hideTooltip = useCallback(() => {
- tooltipRef.current?.classList.add(...HIDDEN);
- }, []);
+ hideTooltipAnimation.current?.restart();
+ }, [hideTooltipAnimation]);
useEffect(() => {
- scroll.current?.on?.(
- 'scroll',
- () => tooltipRef.current?.classList.add(...HIDDEN),
- );
- }, [scroll]);
+ scroll.current?.on?.('scroll', () => {
+ const isVisible =
+ tooltipRef.current &&
+ getComputedStyle(tooltipRef.current).opacity === '1';
+
+ if (isVisible) hideTooltipAnimation.current?.restart();
+ });
+ }, [scroll, hideTooltipAnimation]);
useEffect(() => {
document.addEventListener('mousemove', moveTooltip);
diff --git a/src/shared/lib/helpers/reduceAnimeTargets.ts b/src/shared/lib/helpers/reduceAnimeTargets.ts
new file mode 100644
index 00000000..a575e971
--- /dev/null
+++ b/src/shared/lib/helpers/reduceAnimeTargets.ts
@@ -0,0 +1,42 @@
+import { RefObject } from 'react';
+
+import { IParams } from '../../types/interfaces.ts';
+import { AnimeTarget } from '../../types/types.ts';
+
+/**
+ * Reduces the list of Anime.js targets based on the provided parameters.
+ *
+ * @template {AnimeTarget} T - Generic elementRef
+ * @param {IParams} params - The parameters for reducing the anime targets.
+ * @param {RefObject | null} elementRef - The reference to the anime target element.
+ * @param {boolean} isChildren - Indicates whether to consider the children of the anime target element.
+ *
+ * @returns {T | T[] | null} - The reduced anime target(s).
+ */
+function reduceAnimeTargets(
+ params: IParams,
+ elementRef: RefObject | null = null,
+ isChildren: boolean = false,
+) {
+ let { targets } = params;
+
+ if (elementRef?.current) {
+ targets = elementRef.current;
+
+ if (isChildren && 'children' in elementRef.current) {
+ targets = elementRef.current?.children;
+ }
+ }
+
+ if (params?.targets && 'current' in params.targets) {
+ targets = params.targets.current;
+
+ if (isChildren) {
+ targets = params.targets.current?.children;
+ }
+ }
+
+ return targets as AnimeTarget;
+}
+
+export default reduceAnimeTargets;
diff --git a/src/shared/lib/selectors/selectIsFetchingDetails.ts b/src/shared/lib/selectors/selectIsFetchingDetails.ts
new file mode 100644
index 00000000..d74f9c0a
--- /dev/null
+++ b/src/shared/lib/selectors/selectIsFetchingDetails.ts
@@ -0,0 +1,10 @@
+import { createSelector } from '@reduxjs/toolkit';
+
+import { RootState } from '../../../app/store/store.ts';
+
+const selectIsFetchingMainPage = createSelector(
+ (state: RootState) => state.appReducer.isFetchingDetailsPage,
+ (isFetching) => isFetching,
+);
+
+export default selectIsFetchingMainPage;
diff --git a/src/shared/lib/selectors/selectIsFetchingMain.ts b/src/shared/lib/selectors/selectIsFetchingMain.ts
new file mode 100644
index 00000000..28f8ce53
--- /dev/null
+++ b/src/shared/lib/selectors/selectIsFetchingMain.ts
@@ -0,0 +1,10 @@
+import { createSelector } from '@reduxjs/toolkit';
+
+import { RootState } from '../../../app/store/store.ts';
+
+const selectIsFetchingMain = createSelector(
+ (state: RootState) => state.appReducer.isFetchingMainPage,
+ (isFetching) => isFetching,
+);
+
+export default selectIsFetchingMain;
diff --git a/src/shared/lib/selectors/selectMoviesPerPage.ts b/src/shared/lib/selectors/selectMoviesPerPage.ts
new file mode 100644
index 00000000..2c1d5103
--- /dev/null
+++ b/src/shared/lib/selectors/selectMoviesPerPage.ts
@@ -0,0 +1,10 @@
+import { createSelector } from '@reduxjs/toolkit';
+
+import { RootState } from '../../../app/store/store.ts';
+
+const selectIsFetching = createSelector(
+ (state: RootState) => state.appReducer.moviesPerPage,
+ (moviesPerPage) => moviesPerPage,
+);
+
+export default selectIsFetching;
diff --git a/src/shared/lib/selectors/selectSearchQuery.ts b/src/shared/lib/selectors/selectSearchQuery.ts
new file mode 100644
index 00000000..abf0d282
--- /dev/null
+++ b/src/shared/lib/selectors/selectSearchQuery.ts
@@ -0,0 +1,10 @@
+import { createSelector } from '@reduxjs/toolkit';
+
+import { RootState } from '../../../app/store/store.ts';
+
+const selectSearchQuery = createSelector(
+ (state: RootState) => state.searchReducer.query,
+ (query) => query,
+);
+
+export default selectSearchQuery;
diff --git a/src/shared/types/interfaces.ts b/src/shared/types/interfaces.ts
index 65bcf340..5bff7e29 100644
--- a/src/shared/types/interfaces.ts
+++ b/src/shared/types/interfaces.ts
@@ -1,5 +1,13 @@
import { ReactNode } from 'react';
+import { AnimeParams } from 'animejs';
+
+import { AnimeTarget } from './types.ts';
+
export interface IChildren {
children: ReactNode;
}
+
+export interface IParams extends Omit {
+ targets?: AnimeTarget;
+}
diff --git a/src/shared/types/types.ts b/src/shared/types/types.ts
index eb196191..02352571 100644
--- a/src/shared/types/types.ts
+++ b/src/shared/types/types.ts
@@ -1,3 +1,5 @@
+import { RefObject } from 'react';
+
import { itemsPerPage, urlParams } from './enums.ts';
export type Movie = Readonly<{
@@ -56,3 +58,11 @@ export type ApiMovieResponse = Readonly<{
export type ItemsPerPage = (typeof itemsPerPage)[keyof typeof itemsPerPage];
export type UrlParams = (typeof urlParams)[keyof typeof urlParams];
+
+export type AnimeTarget =
+ | HTMLElement
+ | SVGElement
+ | NodeList
+ | RefObject
+ | HTMLCollection
+ | null;
diff --git a/src/shared/ui/Button.tsx b/src/shared/ui/Button.tsx
index 5f3413e8..fe381f6e 100644
--- a/src/shared/ui/Button.tsx
+++ b/src/shared/ui/Button.tsx
@@ -1,7 +1,9 @@
import { ButtonHTMLAttributes, memo } from 'react';
-import useSearch from '../../features/Search/hooks/useSearch.ts';
+import useAppSelector from '../hooks/useAppSelector.ts';
import cn from '../lib/helpers/cn.ts';
+import selectIsFetchingDetails from '../lib/selectors/selectIsFetchingDetails.ts';
+import selectIsFetchingMain from '../lib/selectors/selectIsFetchingMain.ts';
const buttonTypes = {
filled:
@@ -23,8 +25,11 @@ const Button = memo(function Button({
disabled,
...props
}: IButtonProps) {
- const { isLoading } = useSearch();
- const isDisabled = disabled || isLoading;
+ const isFetchingMain = useAppSelector(selectIsFetchingMain);
+ const isFetchingDetails = useAppSelector(selectIsFetchingDetails);
+
+ const isFetching = isFetchingDetails || isFetchingMain;
+ const isDisabled = disabled || isFetching;
return (