From 4593d12b282c0bc0b2088d26b7a3724cd5f1a128 Mon Sep 17 00:00:00 2001 From: oak Date: Sat, 28 Dec 2024 20:00:13 +0300 Subject: [PATCH 1/6] store and separate component for snackbar created --- src/components/common/snackbar.tsx | 30 ++++++++++++ src/components/login/layout.tsx | 2 + src/components/managers/archive/archive.tsx | 16 +----- .../archive/createArchive/createArchive.tsx | 4 +- .../managers/archive/fetcharchive.ts | 21 +------- .../managers/archive/interfaces/interfaces.ts | 2 - .../archive/listArchive/listArchive.tsx | 3 +- src/components/managers/hero/hero.tsx | 49 +++---------------- .../products/listProducts/allProducts.tsx | 19 ++----- .../filterProducts/filterProducts.tsx | 20 ++------ .../products/productForm/productForm.tsx | 27 ++-------- src/components/managers/promo/createPromo.tsx | 5 +- src/components/managers/promo/listPromo.tsx | 6 ++- src/components/managers/promo/promo.tsx | 24 ++------- src/components/managers/promo/usePromo.ts | 21 +------- src/components/managers/settings/settings.tsx | 21 +------- src/lib/stores/store-types.ts | 13 +++++ src/lib/stores/store.ts | 25 +++++++++- 18 files changed, 110 insertions(+), 198 deletions(-) create mode 100644 src/components/common/snackbar.tsx diff --git a/src/components/common/snackbar.tsx b/src/components/common/snackbar.tsx new file mode 100644 index 00000000..0e960dfb --- /dev/null +++ b/src/components/common/snackbar.tsx @@ -0,0 +1,30 @@ +import { Alert, Snackbar } from '@mui/material'; +import { useSnackBarStore } from 'lib/stores/store'; + +export function SnackBar() { + const { alerts, closeMessage } = useSnackBarStore(); + + return ( + <> + {alerts.map((alert, index) => ( + closeMessage(alert.id)} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + style={{ + bottom: `${index * 60 + 20}px`, + }} + > + closeMessage(alert.id)}> + {alert.message.toUpperCase()} + + + ))} + + ); +} diff --git a/src/components/login/layout.tsx b/src/components/login/layout.tsx index 9fb84b5e..6f456a07 100644 --- a/src/components/login/layout.tsx +++ b/src/components/login/layout.tsx @@ -2,6 +2,7 @@ import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; import ExitToAppIcon from '@mui/icons-material/ExitToApp'; import { AppBar, Box, Button, Container, IconButton, Toolbar, styled } from '@mui/material'; import { useNavigate } from '@tanstack/react-location'; +import { SnackBar } from 'components/common/snackbar'; import { ROUTES } from 'constants/routes'; import logo from 'img/tex-text.png'; import { FC, ReactNode } from 'react'; @@ -60,6 +61,7 @@ export const Layout: FC = ({ children }) => { {children} + ); }; diff --git a/src/components/managers/archive/archive.tsx b/src/components/managers/archive/archive.tsx index 86c312f2..e5d12862 100644 --- a/src/components/managers/archive/archive.tsx +++ b/src/components/managers/archive/archive.tsx @@ -1,4 +1,4 @@ -import { Alert, AppBar, Button, Grid, Snackbar, Toolbar } from '@mui/material'; +import { AppBar, Button, Grid, Toolbar } from '@mui/material'; import { Layout } from 'components/login/layout'; import { FC, useEffect, useState } from 'react'; import { CreateArchive } from './createArchive/createArchive'; @@ -11,11 +11,6 @@ export const Archive: FC = () => { isLoading, hasMore, fetchArchive, - showMessage, - snackBarMessage, - snackBarSeverity, - isSnackBarOpen, - setIsSnackBarOpen, deleteArchiveFromList, deleteItemFromArchive, setArchive, @@ -68,7 +63,6 @@ export const Archive: FC = () => { open={isCreateArchiveModalOpen} close={handleCloseCreateArchiveModal} fetchArchive={fetchArchive} - showMessage={showMessage} /> @@ -78,16 +72,8 @@ export const Archive: FC = () => { deleteArchiveFromList={deleteArchiveFromList} deleteItemFromArchive={deleteItemFromArchive} updateArchiveInformation={updateArchiveInformation} - showMessage={showMessage} /> - setIsSnackBarOpen(!isSnackBarOpen)} - > - {snackBarMessage.toUpperCase()} - ); diff --git a/src/components/managers/archive/createArchive/createArchive.tsx b/src/components/managers/archive/createArchive/createArchive.tsx index f5fe782d..79180325 100644 --- a/src/components/managers/archive/createArchive/createArchive.tsx +++ b/src/components/managers/archive/createArchive/createArchive.tsx @@ -11,13 +11,15 @@ import { CopyToClipboard } from 'components/common/copyToClipboard'; import { Dialog } from 'components/common/dialog'; import { MediaSelectorLayout } from 'components/common/media-selector-layout/layout'; import { TruncateText } from 'components/common/truncateText'; +import { useSnackBarStore } from 'lib/stores/store'; import { FC, useState } from 'react'; import styles from 'styles/archive.scss'; import { ArchiveModal } from '../archiveModal/archiveModal'; import { createArchives } from '../interfaces/interfaces'; import { isValidUrl } from '../utility/isValidUrl'; -export const CreateArchive: FC = ({ fetchArchive, showMessage, open, close }) => { +export const CreateArchive: FC = ({ fetchArchive, open, close }) => { + const { showMessage } = useSnackBarStore(); const initialArchiveState: common_ArchiveNew = { archive: { heading: '', diff --git a/src/components/managers/archive/fetcharchive.ts b/src/components/managers/archive/fetcharchive.ts index 69b4191a..bae1c5d5 100644 --- a/src/components/managers/archive/fetcharchive.ts +++ b/src/components/managers/archive/fetcharchive.ts @@ -1,5 +1,6 @@ import { deleteArchive, getArchive, updateArchive } from 'api/archive'; import { common_ArchiveFull } from 'api/proto-http/frontend'; +import { useSnackBarStore } from 'lib/stores/store'; import { useCallback, useState } from 'react'; import { convertArchiveFullToNew } from './utility/convertArchiveFromFullToNew'; @@ -10,13 +11,8 @@ export const fetchArchives = ( archive: common_ArchiveFull[]; isLoading: boolean; hasMore: boolean; - snackBarMessage: string; - snackBarSeverity: 'success' | 'error'; - isSnackBarOpen: boolean; setArchive: React.Dispatch>; fetchArchive: (limit: number, offset: number) => Promise; - setIsSnackBarOpen: (value: boolean) => void; - showMessage: (message: string, severity: 'success' | 'error') => void; deleteArchiveFromList: (id: number | undefined) => void; deleteItemFromArchive: (archiveId: number | undefined, itemId: number | undefined) => void; updateArchiveInformation: (archiveId: number | undefined, items: common_ArchiveFull) => void; @@ -24,15 +20,7 @@ export const fetchArchives = ( const [archive, setArchive] = useState([]); const [isLoading, setIsLoading] = useState(initialLoading); const [hasMore, setHasMore] = useState(initialHasMore); - const [snackBarMessage, setSnackBarMessage] = useState(''); - const [isSnackBarOpen, setIsSnackBarOpen] = useState(false); - const [snackBarSeverity, setSnackBarSeverity] = useState<'success' | 'error'>('success'); - - const showMessage = (message: string, severity: 'success' | 'error') => { - setSnackBarMessage(message); - setSnackBarSeverity(severity); - setIsSnackBarOpen(!isSnackBarOpen); - }; + const { showMessage } = useSnackBarStore(); const fetchArchive = useCallback(async (limit: number, offset: number) => { setIsLoading(true); @@ -109,13 +97,8 @@ export const fetchArchives = ( archive, isLoading, hasMore, - snackBarMessage, - snackBarSeverity, - isSnackBarOpen, setArchive, fetchArchive, - showMessage, - setIsSnackBarOpen, deleteArchiveFromList, deleteItemFromArchive, updateArchiveInformation, diff --git a/src/components/managers/archive/interfaces/interfaces.ts b/src/components/managers/archive/interfaces/interfaces.ts index e3a8d14d..b6c7f1e7 100644 --- a/src/components/managers/archive/interfaces/interfaces.ts +++ b/src/components/managers/archive/interfaces/interfaces.ts @@ -17,7 +17,6 @@ export interface createArchives { open: boolean; close: () => void; fetchArchive: (limit: number, offset: number) => void; - showMessage: (message: string, severity: 'success' | 'error') => void; } export interface ListArchiveInterface { @@ -26,5 +25,4 @@ export interface ListArchiveInterface { deleteArchiveFromList: (id: number | undefined) => void deleteItemFromArchive: (archiveId: number | undefined, itemId: number | undefined) => void; updateArchiveInformation: (archiveId: number | undefined, items: common_ArchiveFull) => void - showMessage: (message: string, severity: 'success' | 'error') => void; } \ No newline at end of file diff --git a/src/components/managers/archive/listArchive/listArchive.tsx b/src/components/managers/archive/listArchive/listArchive.tsx index e16d1747..33edbaf7 100644 --- a/src/components/managers/archive/listArchive/listArchive.tsx +++ b/src/components/managers/archive/listArchive/listArchive.tsx @@ -6,6 +6,7 @@ import { CopyToClipboard } from 'components/common/copyToClipboard'; import { MediaSelectorLayout } from 'components/common/media-selector-layout/layout'; import { TruncateText } from 'components/common/truncateText'; import { isValidURL } from 'features/utilitty/isValidUrl'; +import { useSnackBarStore } from 'lib/stores/store'; import { FC, useEffect, useState } from 'react'; import styles from 'styles/archiveList.scss'; import { ArchiveModal } from '../archiveModal/archiveModal'; @@ -18,8 +19,8 @@ export const ListArchive: FC = ({ deleteArchiveFromList, deleteItemFromArchive, updateArchiveInformation, - showMessage, }) => { + const { showMessage } = useSnackBarStore(); const [media, setMedia] = useState(''); const [mediaId, setMediaId] = useState(); const [title, setTitle] = useState(''); diff --git a/src/components/managers/hero/hero.tsx b/src/components/managers/hero/hero.tsx index a8d7dd6d..dab84611 100644 --- a/src/components/managers/hero/hero.tsx +++ b/src/components/managers/hero/hero.tsx @@ -1,10 +1,11 @@ -import { Alert, Button, Grid2 as Grid, Snackbar } from '@mui/material'; +import { Button, Grid2 as Grid } from '@mui/material'; import { addHero, getHero } from 'api/hero'; import { common_HeroFullInsert } from 'api/proto-http/admin'; import { common_HeroEntity } from 'api/proto-http/frontend'; import { Layout } from 'components/login/layout'; import { isValidUrlForHero } from 'features/utilitty/isValidUrl'; import { Field, FieldArray, Form, Formik } from 'formik'; +import { useSnackBarStore } from 'lib/stores/store'; import { FC, useEffect, useRef, useState } from 'react'; import styles from 'styles/hero.scss'; import { isValidUrl } from '../archive/utility/isValidUrl'; @@ -14,30 +15,11 @@ import { heroValidationSchema } from './utility/heroValidationShema'; import { mapHeroFunction } from './utility/mapHeroFunction'; export const Hero: FC = () => { + const { showMessage, clearAll } = useSnackBarStore(); const [hero, setHero] = useState(mapHeroFunction()); const [entities, setEntities] = useState([]); - const [alerts, setAlerts] = useState< - Array<{ - message: string; - severity: 'success' | 'error'; - id: number; - }> - >([]); const entityRefs = useRef<{ [key: number]: HTMLDivElement | null }>({}); - const showMessage = (message: string, severity: 'success' | 'error') => { - const newAlert = { - message, - severity, - id: Date.now(), - }; - setAlerts((prev) => [...prev, newAlert]); - }; - - const handleCloseAlert = (id: number) => { - setAlerts((prev) => prev.filter((alert) => alert.id !== id)); - }; - const fetchHero = async () => { const response = await getHero({}); if (!response) return; @@ -90,7 +72,7 @@ export const Hero: FC = () => { const saveHero = async (values: common_HeroFullInsert) => { const { invalidUrls, nonAllowedDomainUrls } = validateExploreLinks(values); - setAlerts([]); + clearAll(); invalidUrls.forEach((message) => { showMessage(message, 'error'); @@ -106,10 +88,10 @@ export const Hero: FC = () => { try { await addHero({ hero: values }); - showMessage('HERO SAVED SUCCESSFULLY', 'success'); + showMessage('hero saved successfully', 'success'); fetchHero(); } catch { - showMessage("HERO CAN'T BE SAVED", 'error'); + showMessage('hero can not be saved', 'error'); } }; @@ -159,25 +141,6 @@ export const Hero: FC = () => { )} - {alerts.map((alert, index) => ( - handleCloseAlert(alert.id)} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'left', - }} - style={{ - bottom: `${index * 60 + 20}px`, - }} - > - handleCloseAlert(alert.id)}> - {alert.message.toUpperCase()} - - - ))} ); }; diff --git a/src/components/managers/products/listProducts/allProducts.tsx b/src/components/managers/products/listProducts/allProducts.tsx index 2f55b4a5..30d311c2 100644 --- a/src/components/managers/products/listProducts/allProducts.tsx +++ b/src/components/managers/products/listProducts/allProducts.tsx @@ -1,8 +1,9 @@ -import { Grid, Snackbar } from '@mui/material'; +import { Grid } from '@mui/material'; import { useNavigate } from '@tanstack/react-location'; import { deleteProductByID } from 'api/admin'; import { GetProductsPagedRequest } from 'api/proto-http/admin'; import { ROUTES } from 'constants/routes'; +import { useSnackBarStore } from 'lib/stores/store'; import debounce from 'lodash/debounce'; import { FC, MouseEvent, useCallback, useEffect, useState } from 'react'; import { Filter } from './filterProducts/filterProducts'; @@ -10,12 +11,11 @@ import { ListProducts } from './listProducts'; import useListProduct from './useListProduct/useListProduct'; export const AllProducts: FC = () => { + const { showMessage } = useSnackBarStore(); const { products, setProducts, filter, setFilter, isLoading, hasMore, fetchProducts } = useListProduct(); const [confirmDelete, setConfirmDelete] = useState(undefined); const [deletingProductId, setDeletingProductId] = useState(undefined); - const [snackbarOpen, setSnackbarOpen] = useState(false); - const [snackbarMessage, setSnackbarMessage] = useState(''); const navigate = useNavigate(); const handleProductClick = (id: number | undefined) => { @@ -41,18 +41,13 @@ export const AllProducts: FC = () => { setProducts((prevProducts) => prevProducts?.filter((product) => product.id !== productId)); setTimeout(() => setDeletingProductId(undefined), 1000); } catch (error) { - setSnackbarMessage('THE PRODUCT CANNOT BE REMOVED'); - setSnackbarOpen(true); + showMessage('the product cannot be removed', 'error'); } finally { setConfirmDelete(undefined); } } }; - const handleSnackbarClose = () => { - setSnackbarOpen(false); - }; - const debouncedFetchProducts = useCallback( debounce((values: GetProductsPagedRequest) => { fetchProducts(50, 0, values); @@ -100,12 +95,6 @@ export const AllProducts: FC = () => { showHidden={filter.showHidden} /> - ); }; diff --git a/src/components/managers/products/listProducts/filterProducts/filterProducts.tsx b/src/components/managers/products/listProducts/filterProducts/filterProducts.tsx index e591fba6..f066fb69 100644 --- a/src/components/managers/products/listProducts/filterProducts/filterProducts.tsx +++ b/src/components/managers/products/listProducts/filterProducts/filterProducts.tsx @@ -12,16 +12,12 @@ import { Select, TextField, } from '@mui/material'; -import { getDictionary } from 'api/admin'; -import { - GetProductsPagedRequest, - common_Dictionary, - common_FilterConditions, -} from 'api/proto-http/admin'; +import { GetProductsPagedRequest, common_FilterConditions } from 'api/proto-http/admin'; import { colors } from 'constants/colors'; import { findInDictionary } from 'features/utilitty/findInDictionary'; import { Field, FieldProps, Form, Formik } from 'formik'; -import { FC, useEffect, useState } from 'react'; +import { useDictionaryStore } from 'lib/stores/store'; +import { FC, useState } from 'react'; import { genderOptions, orderFactors, @@ -34,17 +30,9 @@ interface FilterProps { } export const Filter: FC = ({ filter, onFilterChange }) => { - const [dictionary, setDictionary] = useState(); + const { dictionary } = useDictionaryStore(); const [isOpen, setIsOpen] = useState(true); - useEffect(() => { - const fetchDictionary = async () => { - const response = await getDictionary({}); - setDictionary(response.dictionary); - }; - fetchDictionary(); - }, []); - const handleFieldChange = (setFieldValue: Function, fieldName: string, value: any) => { setFieldValue(fieldName, value); const updatedFilter = { ...filter }; diff --git a/src/components/managers/products/productForm/productForm.tsx b/src/components/managers/products/productForm/productForm.tsx index 4c744d0e..cbb61752 100644 --- a/src/components/managers/products/productForm/productForm.tsx +++ b/src/components/managers/products/productForm/productForm.tsx @@ -1,9 +1,8 @@ -import { Alert, Snackbar } from '@mui/material'; import { MakeGenerics, useMatch } from '@tanstack/react-location'; import { getProductByID, upsertProduct } from 'api/admin'; import { UpsertProductRequest, common_ProductFull, common_ProductNew } from 'api/proto-http/admin'; import { Layout } from 'components/login/layout'; -import { useDictionaryStore } from 'lib/stores/store'; +import { useSnackBarStore } from 'lib/stores/store'; import { FC, useEffect, useState } from 'react'; import { GenericProductForm } from '../genericProductComponent/genericProductComponent'; import { productInitialValues } from '../genericProductComponent/utility/productInitialValues'; @@ -15,7 +14,7 @@ type ProductFormProps = MakeGenerics<{ }>; export const ProductForm: FC = () => { - const { dictionary } = useDictionaryStore(); + const { showMessage } = useSnackBarStore(); const { params: { id }, pathname, @@ -24,15 +23,6 @@ export const ProductForm: FC = () => { const [product, setProduct] = useState(); const [isEditMode, setIsEditMode] = useState(false); const [initialValues, setInitialValues] = useState(productInitialValues()); - const [snackBarMessage, setSnackBarMessage] = useState(''); - const [isSnackBarOpen, setIsSnackBarOpen] = useState(false); - const [snackBarSeverity, setSnackBarSeverity] = useState<'success' | 'error'>('success'); - - const showMessage = (message: string, severity: 'success' | 'error') => { - setSnackBarMessage(message); - setSnackBarSeverity(severity); - setIsSnackBarOpen(true); - }; const fetchProduct = async () => { if (id) { @@ -70,14 +60,14 @@ export const ProductForm: FC = () => { }; if (parseFloat(values.product?.productBody?.price?.value || '') <= 0) { - showMessage('PRICE CANNOT BE ZERO', 'error'); + showMessage('price cannot be zero', 'error'); setSubmitting(false); return; } await upsertProduct(productToSubmit); - showMessage(id && !isCopyMode ? 'PRODUCT UPDATED' : 'PRODUCT UPLOADED', 'success'); + showMessage(id && !isCopyMode ? 'product updated' : 'product uploaded', 'success'); setSubmitting(false); if (!id || (!isCopyMode && !id)) { @@ -88,7 +78,7 @@ export const ProductForm: FC = () => { if (!isCopyMode) fetchProduct(); } catch (error) { showMessage( - id && !isCopyMode ? "PRODUCT CAN'T BE UPDATED" : "PRODUCT CAN'T BE UPLOADED", + id && !isCopyMode ? "product can't be updated" : "product can't be uploaded", 'error', ); } finally { @@ -108,13 +98,6 @@ export const ProductForm: FC = () => { onSubmit={handleFormSubmit} onEditModeChange={setIsEditMode} /> - setIsSnackBarOpen(false)} - > - {snackBarMessage} - ); }; diff --git a/src/components/managers/promo/createPromo.tsx b/src/components/managers/promo/createPromo.tsx index 13365c0b..7cb474d5 100644 --- a/src/components/managers/promo/createPromo.tsx +++ b/src/components/managers/promo/createPromo.tsx @@ -10,11 +10,11 @@ import { import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; import { common_PromoCodeInsert } from 'api/proto-http/admin'; +import { useSnackBarStore } from 'lib/stores/store'; import { FC, useState } from 'react'; interface CreatePromoInterface { createNewPromo: (newPromo: common_PromoCodeInsert) => void; - showMessage: (message: string, severity: 'success' | 'error') => void; } const addMonths = (date: Date, months: number) => { @@ -23,7 +23,8 @@ const addMonths = (date: Date, months: number) => { return newDate; }; -export const CreatePromo: FC = ({ showMessage, createNewPromo }) => { +export const CreatePromo: FC = ({ createNewPromo }) => { + const { showMessage } = useSnackBarStore(); const defaultDate = addMonths(new Date(), 3); const initialPromoStates: common_PromoCodeInsert = { code: '', diff --git a/src/components/managers/promo/listPromo.tsx b/src/components/managers/promo/listPromo.tsx index c74ef600..d54a8656 100644 --- a/src/components/managers/promo/listPromo.tsx +++ b/src/components/managers/promo/listPromo.tsx @@ -3,15 +3,17 @@ import { Grid, IconButton } from '@mui/material'; import { DataGrid } from '@mui/x-data-grid'; import { deletePromo } from 'api/promo'; import { common_PromoCode } from 'api/proto-http/admin'; +import { useSnackBarStore } from 'lib/stores/store'; import { FC, useCallback } from 'react'; interface ListPromosInterface { promos: common_PromoCode[]; fetchPromos: (limit: number, offset: number) => void; - showMessage: (message: string, severity: 'success' | 'error') => void; } -export const ListPromo: FC = ({ promos, fetchPromos, showMessage }) => { +export const ListPromo: FC = ({ promos, fetchPromos }) => { + const { showMessage } = useSnackBarStore(); + const transformPromoForDataGrid = promos.map((promo, index) => ({ id: index, code: promo.promoCodeInsert?.code, diff --git a/src/components/managers/promo/promo.tsx b/src/components/managers/promo/promo.tsx index 5a1d0153..656afa3b 100644 --- a/src/components/managers/promo/promo.tsx +++ b/src/components/managers/promo/promo.tsx @@ -1,4 +1,4 @@ -import { Alert, Grid, Snackbar } from '@mui/material'; +import { Grid } from '@mui/material'; import { Layout } from 'components/login/layout'; import { FC, useEffect } from 'react'; import { CreatePromo } from './createPromo'; @@ -6,16 +6,7 @@ import { ListPromo } from './listPromo'; import usePromo from './usePromo'; export const Promo: FC = () => { - const { - promos, - snackBarMessage, - snackBarSeverity, - isSnackBarOpen, - fetchPromos, - createNewPromo, - setIsSnackBarOpen, - showMessage, - } = usePromo(); + const { promos, fetchPromos, createNewPromo } = usePromo(); useEffect(() => { fetchPromos(50, 0); @@ -25,19 +16,12 @@ export const Promo: FC = () => { - + - + - setIsSnackBarOpen(!isSnackBarOpen)} - > - {snackBarMessage} - ); }; diff --git a/src/components/managers/promo/usePromo.ts b/src/components/managers/promo/usePromo.ts index 08fa7c2a..0f930019 100644 --- a/src/components/managers/promo/usePromo.ts +++ b/src/components/managers/promo/usePromo.ts @@ -1,29 +1,17 @@ import { addPromo, getPromo } from "api/promo"; import { common_PromoCode, common_PromoCodeInsert } from "api/proto-http/admin"; +import { useSnackBarStore } from "lib/stores/store"; import { useCallback, useState } from "react"; const usePromo = (initialIsLoading = false, initialHasMore = true): { promos: common_PromoCode[]; - snackBarMessage: string; - snackBarSeverity: 'success' | 'error'; - isSnackBarOpen: boolean; fetchPromos: (limit: number, offset: number) => void createNewPromo: (newPromo: common_PromoCodeInsert) => void - setIsSnackBarOpen: (value: boolean) => void; - showMessage: (message: string, severity: 'success' | 'error') => void; } => { + const { showMessage } = useSnackBarStore(); const [promos, setPromos] = useState([]) const [isLoading, setIsLoading] = useState(initialIsLoading); const [hasMore, setHasMore] = useState(initialHasMore); - const [snackBarMessage, setSnackBarMessage] = useState(''); - const [isSnackBarOpen, setIsSnackBarOpen] = useState(false); - const [snackBarSeverity, setSnackBarSeverity] = useState<'success' | 'error'>('success'); - - const showMessage = (message: string, severity: 'success' | 'error') => { - setSnackBarMessage(message); - setSnackBarSeverity(severity); - setIsSnackBarOpen(true); - }; const fetchPromos = useCallback(async (limit: number, startOffset: number) => { setIsLoading(true) @@ -52,13 +40,8 @@ const usePromo = (initialIsLoading = false, initialHasMore = true): { return { promos, - snackBarMessage, - snackBarSeverity, - isSnackBarOpen, fetchPromos, createNewPromo, - setIsSnackBarOpen, - showMessage } } diff --git a/src/components/managers/settings/settings.tsx b/src/components/managers/settings/settings.tsx index 2dba05c1..cb26d475 100644 --- a/src/components/managers/settings/settings.tsx +++ b/src/components/managers/settings/settings.tsx @@ -1,10 +1,8 @@ import { - Alert, Box, Checkbox, FormControlLabel, Grid, - Snackbar, TextField, Theme, Typography, @@ -14,28 +12,20 @@ import { UpdateSettingsRequest } from 'api/proto-http/admin'; import { updateSettings } from 'api/settings'; import { Layout } from 'components/login/layout'; import { Field, FieldProps, Formik } from 'formik'; -import { useDictionaryStore } from 'lib/stores/store'; +import { useDictionaryStore, useSnackBarStore } from 'lib/stores/store'; import debounce from 'lodash/debounce'; import { FC, useCallback, useEffect, useState } from 'react'; import { defaultSettingsStates } from './defaultSettingsStates'; import { usePaymentMethodsMapping, useShipmentCarriersMapping } from './mappingFunctions'; export const Settings: FC = () => { + const { showMessage } = useSnackBarStore(); const { dictionary } = useDictionaryStore(); const [settings, setSettings] = useState(defaultSettingsStates); const shipmentCarriers = useShipmentCarriersMapping(); const paymentMethods = usePaymentMethodsMapping(); - const [snackBarMessage, setSnackBarMessage] = useState(''); - const [isSnackBarOpen, setIsSnackBarOpen] = useState(false); - const [snackBarSeverity, setSnackBarSeverity] = useState<'success' | 'error'>('success'); const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm')); - const showMessage = (message: string, severity: 'success' | 'error') => { - setSnackBarMessage(message); - setSnackBarSeverity(severity); - setIsSnackBarOpen(true); - }; - useEffect(() => { setSettings((prev) => ({ ...prev, @@ -222,13 +212,6 @@ export const Settings: FC = () => { )} - setIsSnackBarOpen(!isSnackBarOpen)} - > - {snackBarMessage} - ); }; diff --git a/src/lib/stores/store-types.ts b/src/lib/stores/store-types.ts index fa3848a5..349afffb 100644 --- a/src/lib/stores/store-types.ts +++ b/src/lib/stores/store-types.ts @@ -6,4 +6,17 @@ export interface DictionaryStore { error: string | null; initialized: boolean; fetchDictionary: () => Promise; +} + +interface Alert { + message: string; + severity: 'success' | 'error'; + id: number; +} + +export interface SnackBarStore { + alerts: Alert[]; + showMessage: (message: string, severity: 'success' | 'error') => void; + closeMessage: (id: number) => void; + clearAll: () => void; } \ No newline at end of file diff --git a/src/lib/stores/store.ts b/src/lib/stores/store.ts index ce84ccc8..b4183c7b 100644 --- a/src/lib/stores/store.ts +++ b/src/lib/stores/store.ts @@ -1,7 +1,7 @@ import { getDictionary } from "api/admin"; import { create } from "zustand"; -import { DictionaryStore } from "./store-types"; +import { DictionaryStore, SnackBarStore } from "./store-types"; export const useDictionaryStore = create((set, get) => ({ dictionary: undefined, @@ -18,4 +18,25 @@ export const useDictionaryStore = create((set, get) => ({ set({ error: 'Failed to fetch dictionary', loading: false, initialized: false }) } } -})) \ No newline at end of file +})) + + +export const useSnackBarStore = create((set) => ({ + alerts: [], + showMessage: (message: string, severity: 'success' | 'error') => { + const newAlert = { + message, + severity, + id: Date.now(), + }; + set((state) => ({ alerts: [...state.alerts, newAlert] })); + }, + closeMessage: (id: number) => { + set((state) => ({ + alerts: state.alerts.filter((alert) => alert.id !== id) + })); + }, + clearAll: () => { + set({ alerts: [] }); + } +})); \ No newline at end of file From 08f0e7574b0512eda28818fea05dfefb03bac8e5 Mon Sep 17 00:00:00 2001 From: oak Date: Sat, 28 Dec 2024 20:08:28 +0300 Subject: [PATCH 2/6] wip --- .../media-selector-components/dragDrop.tsx | 22 +++------------- .../fullSizeMediaModal.tsx | 15 +---------- .../media-selector-components/listMedia.tsx | 10 ++------ .../mediaSelectorModal.tsx | 9 ++----- src/features/utilitty/aspectRatioDisplay.tsx | 10 -------- src/features/utilitty/useMediaSelector.ts | 25 ++----------------- 6 files changed, 10 insertions(+), 81 deletions(-) delete mode 100644 src/features/utilitty/aspectRatioDisplay.tsx diff --git a/src/components/common/media-selector-layout/media-selector-components/dragDrop.tsx b/src/components/common/media-selector-layout/media-selector-components/dragDrop.tsx index f7071e8b..90ee0ff4 100644 --- a/src/components/common/media-selector-layout/media-selector-components/dragDrop.tsx +++ b/src/components/common/media-selector-layout/media-selector-components/dragDrop.tsx @@ -1,16 +1,15 @@ import { - Alert, Box, Button, CircularProgress, Grid, Paper, - Snackbar, Theme, Typography, useMediaQuery, } from '@mui/material'; import { getBase64File } from 'features/utilitty/getBase64'; +import { useSnackBarStore } from 'lib/stores/store'; import React, { Dispatch, FC, SetStateAction, useState } from 'react'; interface DragDropProps { @@ -28,22 +27,10 @@ export const DragDrop: FC = ({ setSelectedFiles, loading, }) => { + const { showMessage } = useSnackBarStore(); const [isDragging, setIsDragging] = useState(false); - const [snackbarOpen, setSnackbarOpen] = useState(false); - const [snackbarMessage, setSnackbarMessage] = useState(''); - const [snackBarSeverity, setSnackBarSeverity] = useState<'success' | 'error'>('success'); const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm')); - const handleSnackbarClose = () => { - setSnackbarOpen(false); - }; - - const showMessage = (message: string, severity: 'success' | 'error') => { - setSnackbarMessage(message); - setSnackBarSeverity(severity); - setSnackbarOpen(true); - }; - const processFiles = async (files: FileList) => { if (files && files.length > 0) { const file = files[0]; @@ -69,7 +56,7 @@ export const DragDrop: FC = ({ if (files && files.length > 0) { processFiles(files); } else { - showMessage('NO SELECTED FILES', 'error'); + showMessage('no files selected', 'error'); } if ('dataTransfer' in e) { setIsDragging(false); @@ -123,9 +110,6 @@ export const DragDrop: FC = ({ {loading && } - - {snackbarMessage} - ); diff --git a/src/components/common/media-selector-layout/media-selector-components/fullSizeMediaModal.tsx b/src/components/common/media-selector-layout/media-selector-components/fullSizeMediaModal.tsx index 27b23a0a..616120fe 100644 --- a/src/components/common/media-selector-layout/media-selector-components/fullSizeMediaModal.tsx +++ b/src/components/common/media-selector-layout/media-selector-components/fullSizeMediaModal.tsx @@ -1,4 +1,4 @@ -import { Grid2 as Grid, Snackbar, Theme, Typography, useMediaQuery } from '@mui/material'; +import { Grid2 as Grid, Theme, Typography, useMediaQuery } from '@mui/material'; import { common_MediaInfo, common_MediaItem } from 'api/proto-http/admin'; import { CopyToClipboard } from 'components/common/copyToClipboard'; import { PreviewMediaForUpload } from 'components/common/cropper/previewMediaForUpload'; @@ -20,8 +20,6 @@ export const FullSizeMediaModal: FC = ({ setCroppedImage, handleUploadMedia, }) => { - const [snackBarOpen, setSnackbarOpen] = useState(false); - const [snackBarMessage, setSnackBarMessage] = useState(''); const [videoDimensions, setVideoDimensions] = useState({}); const [isCropperOpen, setIsCropperOpen] = useState(false); const [isPreviewOpen, setIsPreviewOpen] = useState(true); @@ -54,11 +52,6 @@ export const FullSizeMediaModal: FC = ({ } }, [clickedMedia]); - const showMessage = (message: string) => { - setSnackBarMessage(message); - setSnackbarOpen(true); - }; - const clearDragDropSelector = () => { setCroppedImage(''); setIsPreviewOpen(!isPreviewOpen); @@ -120,12 +113,6 @@ export const FullSizeMediaModal: FC = ({ ))} - setSnackbarOpen(false)} - message={snackBarMessage} - /> ); }; diff --git a/src/components/common/media-selector-layout/media-selector-components/listMedia.tsx b/src/components/common/media-selector-layout/media-selector-components/listMedia.tsx index cfb64e5d..2f5a273e 100644 --- a/src/components/common/media-selector-layout/media-selector-components/listMedia.tsx +++ b/src/components/common/media-selector-layout/media-selector-components/listMedia.tsx @@ -1,14 +1,12 @@ import CheckIcon from '@mui/icons-material/Check'; import ClearIcon from '@mui/icons-material/Clear'; import { - Alert, Box, Grid, IconButton, ImageList, ImageListItem, InputLabel, - Snackbar, Typography, useMediaQuery, useTheme, @@ -18,7 +16,7 @@ import { common_MediaFull, common_MediaItem } from 'api/proto-http/admin'; import { MediaSelectorMediaListProps } from 'components/common/interfaces/mediaSelectorInterfaces'; import { calculateAspectRatio } from 'features/utilitty/calculateAspectRatio'; import { isVideo } from 'features/utilitty/filterContentType'; -import useMediaSelector from 'features/utilitty/useMediaSelector'; +import { useSnackBarStore } from 'lib/stores/store'; import { FC, useCallback, useEffect, useState } from 'react'; import styles from 'styles/media-selector.scss'; import { FullSizeMediaModal } from './fullSizeMediaModal'; @@ -37,8 +35,7 @@ export const MediaList: FC = ({ sortedAndFilteredMedia, handleUploadMedia, }) => { - const { isSnackBarOpen, snackBarMessage, snackBarSeverity, showMessage, closeSnackBar } = - useMediaSelector(); + const { showMessage } = useSnackBarStore(); const [openModal, setOpenModal] = useState(false); const [clickedMedia, setClickedMedia] = useState(); const theme = useTheme(); @@ -223,9 +220,6 @@ export const MediaList: FC = ({ setCroppedImage={setCroppedImage} handleUploadMedia={handleUploadMedia} /> - - {snackBarMessage} - ); }; diff --git a/src/components/common/media-selector-layout/media-selector-components/mediaSelectorModal.tsx b/src/components/common/media-selector-layout/media-selector-components/mediaSelectorModal.tsx index 8cf47bae..2f6d2c16 100644 --- a/src/components/common/media-selector-layout/media-selector-components/mediaSelectorModal.tsx +++ b/src/components/common/media-selector-layout/media-selector-components/mediaSelectorModal.tsx @@ -1,8 +1,7 @@ -import { Alert, Snackbar } from '@mui/material'; import { common_MediaFull } from 'api/proto-http/admin'; import { Dialog } from 'components/common/dialog'; import { MediaSelectorModalProps } from 'components/common/interfaces/mediaSelectorInterfaces'; -import useMediaSelector from 'features/utilitty/useMediaSelector'; +import { useSnackBarStore } from 'lib/stores/store'; import { FC, useState } from 'react'; import { MediaSelector } from './mediaSelector'; @@ -14,8 +13,7 @@ export const MediaSelectorModal: FC = ({ closeMediaSelector, saveSelectedMedia, }) => { - const { snackBarMessage, closeSnackBar, isSnackBarOpen, showMessage, snackBarSeverity } = - useMediaSelector(); + const { showMessage } = useSnackBarStore(); const [selectedMedia, setSelectedMedia] = useState([]); const [open, setOpen] = useState(true); @@ -53,9 +51,6 @@ export const MediaSelectorModal: FC = ({ selectedMedia={selectedMedia} select={select} /> - - {snackBarMessage} - ); }; diff --git a/src/features/utilitty/aspectRatioDisplay.tsx b/src/features/utilitty/aspectRatioDisplay.tsx deleted file mode 100644 index 3d3492e7..00000000 --- a/src/features/utilitty/aspectRatioDisplay.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { FC } from 'react'; - -interface AspectRatio { - width: string | undefined; - height: string | undefined; -} - -export const AspectRatioDisplay: FC = ({ width, height }) => { - return
AspectRatioDisplay
; -}; diff --git a/src/features/utilitty/useMediaSelector.ts b/src/features/utilitty/useMediaSelector.ts index a7a2ebe1..3154df9b 100644 --- a/src/features/utilitty/useMediaSelector.ts +++ b/src/features/utilitty/useMediaSelector.ts @@ -4,6 +4,7 @@ import { uploadContentVideo } from 'api/admin'; import { common_MediaFull } from 'api/proto-http/admin'; +import { useSnackBarStore } from 'lib/stores/store'; import { Dispatch, SetStateAction, useCallback, useState } from 'react'; import { checkIsHttpHttpsMediaLink } from './checkIsHttpHttpsLink'; import { isVideo } from './filterContentType'; @@ -30,10 +31,7 @@ const useMediaSelector = ( croppedImage: string | null, filterByType: string; sortByDate: string; - snackBarMessage: string; - snackBarSeverity: 'success' | 'error'; isLoading: boolean; - isSnackBarOpen: boolean; loading: boolean, setMedia: React.Dispatch>; setUrl: React.Dispatch>; @@ -44,35 +42,21 @@ const useMediaSelector = ( setFilterByType: React.Dispatch>; setSortByDate: React.Dispatch>; sortedAndFilteredMedia: () => common_MediaFull[]; - showMessage: (message: string, severity: 'success' | 'error') => void; - closeSnackBar: () => void; setSelectedFileUrl: (url: string) => void; setCroppedImage: (img: string | null) => void; } => { + const { showMessage } = useSnackBarStore(); const [media, setMedia] = useState([]); const [isLoading, setIsLoading] = useState(initialIsLoading); const [hasMore, setHasMore] = useState(initialHasMore); const [url, setUrl] = useState(''); const [filterByType, setFilterByType] = useState(''); const [sortByDate, setSortByDate] = useState('desc'); - const [snackBarMessage, setSnackBarMessage] = useState(''); - const [isSnackBarOpen, setIsSnackBarOpen] = useState(false); - const [snackBarSeverity, setSnackBarSeverity] = useState<'success' | 'error'>('success'); const [loading, setLoading] = useState(false); const [selectedFiles, setSelectedFiles] = useState([]); const [croppedImage, setCroppedImage] = useState(null); const [selectedFileUrl, setSelectedFileUrl] = useState(''); - const showMessage = (message: string, severity: 'success' | 'error') => { - setSnackBarMessage(message); - setSnackBarSeverity(severity); - setIsSnackBarOpen(!isSnackBarOpen); - }; - - const closeSnackBar = () => { - setIsSnackBarOpen(!isSnackBarOpen); - }; - const sortedAndFilteredMedia = useCallback(() => { return media ?.filter((m) => { @@ -160,12 +144,9 @@ const useMediaSelector = ( url, selectedFileUrl, croppedImage, - snackBarSeverity, - snackBarMessage, filterByType, sortByDate, isLoading, - isSnackBarOpen, loading, fetchFiles, reload, @@ -174,8 +155,6 @@ const useMediaSelector = ( setFilterByType, setSortByDate, sortedAndFilteredMedia, - showMessage, - closeSnackBar, setSelectedFileUrl, setCroppedImage, setSelectedFiles, From 15099950b8f3ce477bc505b263fb3765b1a54444 Mon Sep 17 00:00:00 2001 From: oak Date: Tue, 31 Dec 2024 14:13:22 +0300 Subject: [PATCH 3/6] featured archive added in hero --- proto | 2 +- .../managers/hero/entities/entities.tsx | 35 ++++++ .../featured-archive/archive-picker.tsx | 102 ++++++++++++++++++ .../featured-archive/featured-archive.tsx | 28 +++++ .../heroProductsTable.tsx | 1 - .../hero/entities/interface/interface.ts | 13 ++- .../managers/hero/utility/mapHeroFunction.ts | 15 ++- .../utility/validationForSelectHeroType.ts | 3 + 8 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 src/components/managers/hero/entities/featured-archive/archive-picker.tsx create mode 100644 src/components/managers/hero/entities/featured-archive/featured-archive.tsx diff --git a/proto b/proto index 0ee0c1a8..d4342584 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 0ee0c1a8969e3faee2b8d8e3f458db215d9b5825 +Subproject commit d43425846480b211a15367a29ed03b4d68b9a623 diff --git a/src/components/managers/hero/entities/entities.tsx b/src/components/managers/hero/entities/entities.tsx index 9976b420..09d65307 100644 --- a/src/components/managers/hero/entities/entities.tsx +++ b/src/components/managers/hero/entities/entities.tsx @@ -1,5 +1,6 @@ import { Box, Button, Divider, Grid2 as Grid, TextField } from '@mui/material'; import { common_HeroFullInsert, common_MediaFull, common_Product } from 'api/proto-http/admin'; +import { common_ArchiveFull } from 'api/proto-http/frontend'; import { calculateAspectRatio } from 'features/utilitty/calculateAspectRatio'; import { Field, useFormikContext } from 'formik'; import { FC, useEffect, useState } from 'react'; @@ -8,6 +9,7 @@ import { removeEntityIndex } from '../utility/arrayHelpers'; import { getAllowedRatios } from '../utility/getAllowedRatios'; import { createMediaSaveConfigs } from '../utility/save-media-config'; import { CommonEntity } from './common-entity/common-entity'; +import { FeaturedArchive } from './featured-archive/featured-archive'; import { FeaturedProductBase } from './featured-products-(tags)/featured-prduct-base'; import { EntitiesProps } from './interface/interface'; @@ -20,6 +22,7 @@ export const Entities: FC = ({ entityRefs, entities, arrayHelpers }>({}); const [product, setProduct] = useState<{ [key: number]: common_Product[] }>({}); const [productTags, setProductTags] = useState<{ [key: number]: common_Product[] }>({}); + const [archive, setArchive] = useState<{ [key: number]: common_ArchiveFull[] }>({}); const [currentEntityIndex, setCurrentEntityIndex] = useState(null); const [allowedRatios, setAllowedRatios] = useState<{ [key: number]: string[] }>({}); const [isModalOpen, setIsModalOpen] = useState(false); @@ -39,6 +42,7 @@ export const Entities: FC = ({ entityRefs, entities, arrayHelpers const doubleAddEntities: { [key: number]: { left: string; right: string } } = {}; const productsForEntities: { [key: number]: common_Product[] } = {}; const productTagsForEntities: { [key: number]: common_Product[] } = {}; + const archiveForEntities: { [key: number]: common_ArchiveFull[] } = {}; const calculatedAllowedRatios: { [key: number]: string[] } = {}; entities.forEach((e, i) => { @@ -51,6 +55,12 @@ export const Entities: FC = ({ entityRefs, entities, arrayHelpers productsForEntities[i] = e.featuredProducts?.products || []; productTagsForEntities[i] = e.featuredProductsTag?.products?.products || []; + archiveForEntities[i] = [ + { + archive: e.featuredArchive?.archive?.archive, + items: e.featuredArchive?.archive?.items || [], + }, + ]; const allowedRatios = getAllowedRatios(e); if (allowedRatios.length > 0) { @@ -64,6 +74,7 @@ export const Entities: FC = ({ entityRefs, entities, arrayHelpers setProduct(productsForEntities); setProductTags(productTagsForEntities); setAllowedRatios(calculatedAllowedRatios); + setArchive(archiveForEntities); }; useEffect(() => { @@ -141,6 +152,16 @@ export const Entities: FC = ({ entityRefs, entities, arrayHelpers arrayHelpers.remove(index); }; + const handleOpenArchiveSelection = (index: number) => { + setCurrentEntityIndex(index); + setIsModalOpen(true); + }; + + const handleSaveArchive = (newSelectedArchive: common_ArchiveFull[], index: number) => { + setFieldValue(`entities.${index}.featuredArchive.archiveId`, newSelectedArchive[0].archive?.id); + handleCloseModal(); + }; + return ( {values.entities && @@ -249,6 +270,20 @@ export const Entities: FC = ({ entityRefs, entities, arrayHelpers /> )} + {entity.type === 'HERO_TYPE_FEATURED_ARCHIVE' && ( + + + + )} + + ); +} diff --git a/src/components/managers/hero/entities/featured-archive/featured-archive.tsx b/src/components/managers/hero/entities/featured-archive/featured-archive.tsx new file mode 100644 index 00000000..fb60b1e8 --- /dev/null +++ b/src/components/managers/hero/entities/featured-archive/featured-archive.tsx @@ -0,0 +1,28 @@ +import { Button } from '@mui/material'; +import { FeatureArchiveProps } from '../interface/interface'; +import { ArchivePicker } from './archive-picker'; + +export function FeaturedArchive({ + archive, + index, + currentEntityIndex, + open, + onClose, + handleSaveArchiveSelection, + handleOpenArchiveSelection, +}: FeatureArchiveProps) { + return ( +
+ {archive[index]?.[0].archive?.id} + +
+ handleSaveArchiveSelection(selectedArchive, index)} + selectedArchiveId={archive[index]?.[0].archive?.id ?? 0} + /> +
+
+ ); +} diff --git a/src/components/managers/hero/entities/featured-products-(tags)/heroProductsTable.tsx b/src/components/managers/hero/entities/featured-products-(tags)/heroProductsTable.tsx index 4bf07d21..294c68b7 100644 --- a/src/components/managers/hero/entities/featured-products-(tags)/heroProductsTable.tsx +++ b/src/components/managers/hero/entities/featured-products-(tags)/heroProductsTable.tsx @@ -30,7 +30,6 @@ export const HeroProductTable: FC< const { setFieldValue } = useFormikContext(); const categories = useDictionaryStore((state) => state.dictionary?.categories || []); const navigate = useNavigate(); - const [data, setData] = useState(products); useEffect(() => { diff --git a/src/components/managers/hero/entities/interface/interface.ts b/src/components/managers/hero/entities/interface/interface.ts index 1ad49c3e..0a165399 100644 --- a/src/components/managers/hero/entities/interface/interface.ts +++ b/src/components/managers/hero/entities/interface/interface.ts @@ -1,5 +1,5 @@ import { common_MediaFull, common_Product } from "api/proto-http/admin"; -import { common_HeroEntity } from "api/proto-http/frontend"; +import { common_ArchiveFull, common_HeroEntity } from "api/proto-http/frontend"; import { FieldArrayRenderProps } from "formik"; export interface EntitiesProps { @@ -32,3 +32,14 @@ export interface HeroProductEntityInterface { handleCloseModal?: () => void; handleSaveNewSelection?: (selectedProduct: common_Product[], index: number) => void; } + + +export interface FeatureArchiveProps { + archive: { [key: number]: common_ArchiveFull[] }; + index: number; + currentEntityIndex: number | null; + open: boolean; + onClose: () => void; + handleSaveArchiveSelection: (newSelectedArchive: common_ArchiveFull[], index: number) => void; + handleOpenArchiveSelection: (index: number) => void; +} diff --git a/src/components/managers/hero/utility/mapHeroFunction.ts b/src/components/managers/hero/utility/mapHeroFunction.ts index eaf73faa..474137f4 100644 --- a/src/components/managers/hero/utility/mapHeroFunction.ts +++ b/src/components/managers/hero/utility/mapHeroFunction.ts @@ -7,7 +7,8 @@ export const heroTypes: { value: common_HeroType; label: string }[] = [ { value: 'HERO_TYPE_SINGLE', label: 'single add' }, { value: 'HERO_TYPE_DOUBLE', label: 'double add' }, { value: 'HERO_TYPE_FEATURED_PRODUCTS', label: 'featured products' }, - { value: 'HERO_TYPE_FEATURED_PRODUCTS_TAG', label: 'featured products tag' } + { value: 'HERO_TYPE_FEATURED_PRODUCTS_TAG', label: 'featured products tag' }, + { value: 'HERO_TYPE_FEATURED_ARCHIVE', label: 'featured archive' } ] export const mapHeroFunction = (hero?: common_HeroFull | undefined): common_HeroFullInsert => { @@ -58,6 +59,12 @@ export const mapHeroFunction = (hero?: common_HeroFull | undefined): common_Hero headline: entity.featuredProductsTag?.products?.headline, exploreLink: entity.featuredProductsTag?.products?.exploreLink, exploreText: entity.featuredProductsTag?.products?.exploreText, + }, + featuredArchive: { + archiveId: entity.featuredArchive?.archive?.items?.[0].archiveId, + tag: entity.featuredArchive?.tag, + headline: entity.featuredArchive?.headline, + exploreText: entity.featuredArchive?.exploreText, } })), }; @@ -108,6 +115,12 @@ export const emptyHeroForm: common_HeroFullInsert = { headline: '', exploreLink: '', exploreText: '' + }, + featuredArchive: { + archiveId: 0, + tag: '', + headline: '', + exploreText: '' } } ] diff --git a/src/components/managers/hero/utility/validationForSelectHeroType.ts b/src/components/managers/hero/utility/validationForSelectHeroType.ts index bf233d86..7ef083e1 100644 --- a/src/components/managers/hero/utility/validationForSelectHeroType.ts +++ b/src/components/managers/hero/utility/validationForSelectHeroType.ts @@ -17,5 +17,8 @@ export const validationForSelectHeroType: Record !entity.featuredProductsTag?.tag, + HERO_TYPE_FEATURED_ARCHIVE: (entity: common_HeroEntityInsert) => + !entity.featuredArchive?.archiveId, + HERO_TYPE_UNKNOWN: (entity: common_HeroEntityInsert) => false, } \ No newline at end of file From d9793e6c52ae7857e16756036c38be772d2cf555 Mon Sep 17 00:00:00 2001 From: oak Date: Fri, 3 Jan 2025 17:27:59 +0300 Subject: [PATCH 4/6] archive fields and display of selected archive added --- .../managers/hero/entities/entities.tsx | 6 + .../featured-archive/archive-picker.tsx | 35 ++++-- .../featured-archive/featured-archive.tsx | 113 ++++++++++++++++-- .../hero/entities/interface/interface.ts | 1 + 4 files changed, 138 insertions(+), 17 deletions(-) diff --git a/src/components/managers/hero/entities/entities.tsx b/src/components/managers/hero/entities/entities.tsx index 09d65307..6389451a 100644 --- a/src/components/managers/hero/entities/entities.tsx +++ b/src/components/managers/hero/entities/entities.tsx @@ -148,6 +148,7 @@ export const Entities: FC = ({ entityRefs, entities, arrayHelpers setDoubleAdd((prevDoubleAdd) => removeEntityIndex(prevDoubleAdd, index)); setProduct((prevProduct) => removeEntityIndex(prevProduct, index)); setProductTags((prevProductTags) => removeEntityIndex(prevProductTags, index)); + setArchive((prevArchive) => removeEntityIndex(prevArchive, index)); arrayHelpers.remove(index); }; @@ -159,6 +160,10 @@ export const Entities: FC = ({ entityRefs, entities, arrayHelpers const handleSaveArchive = (newSelectedArchive: common_ArchiveFull[], index: number) => { setFieldValue(`entities.${index}.featuredArchive.archiveId`, newSelectedArchive[0].archive?.id); + setArchive((prevState) => ({ + ...prevState, + [index]: newSelectedArchive, + })); handleCloseModal(); }; @@ -275,6 +280,7 @@ export const Entities: FC = ({ entityRefs, entities, arrayHelpers { - setCurrentPage((prevCurrentPage) => prevCurrentPage + 1); - }; - function handleSave() { if (!selectedArchive) return; onSave([selectedArchive]); @@ -86,17 +82,36 @@ export function ArchivePicker({ open, onClose, onSave, selectedArchiveId }: Prop enableGlobalFilter: false, }, { - accessorKey: 'archive.archiveBody.description', - header: 'Description', + accessorKey: 'items', + header: 'Items quantity', + Cell: ({ row }) => { + return row.original.items?.length; + }, }, ], [selectedArchive], ); + const table = useMaterialReactTable({ + autoResetPageIndex: false, + columns, + data, + initialState: { + pagination: { + pageSize: 50, + pageIndex: 1, + }, + }, + muiPaginationProps: { + rowsPerPageOptions: [50, 100, 200], + showFirstButton: false, + showLastButton: false, + }, + }); + return ( - - + ); } diff --git a/src/components/managers/hero/entities/featured-archive/featured-archive.tsx b/src/components/managers/hero/entities/featured-archive/featured-archive.tsx index fb60b1e8..96bb7b51 100644 --- a/src/components/managers/hero/entities/featured-archive/featured-archive.tsx +++ b/src/components/managers/hero/entities/featured-archive/featured-archive.tsx @@ -1,9 +1,15 @@ -import { Button } from '@mui/material'; +import { Button, Grid2 as Grid, TextField, Typography } from '@mui/material'; +import { CopyToClipboard } from 'components/common/copyToClipboard'; +import { TruncateText } from 'components/common/truncateText'; +import { Field } from 'formik'; +import styles from 'styles/archiveList.scss'; +import { HeroProductTable } from '../featured-products-(tags)/heroProductsTable'; import { FeatureArchiveProps } from '../interface/interface'; import { ArchivePicker } from './archive-picker'; export function FeaturedArchive({ archive, + product, index, currentEntityIndex, open, @@ -12,17 +18,110 @@ export function FeaturedArchive({ handleOpenArchiveSelection, }: FeatureArchiveProps) { return ( -
- {archive[index]?.[0].archive?.id} - -
+ + + + featured archive + + + + {`items[${archive[index]?.[0].items?.length}]`} + + + handleSaveArchiveSelection(selectedArchive, index)} selectedArchiveId={archive[index]?.[0].archive?.id ?? 0} /> -
-
+
+ + + {archive[index]?.[0].items?.map((item) => ( + + + + + + + {item.archiveItem?.url && ( + + )} + + + ))} + + + + + + + + + + + + + + + ); } diff --git a/src/components/managers/hero/entities/interface/interface.ts b/src/components/managers/hero/entities/interface/interface.ts index 0a165399..3e48a15e 100644 --- a/src/components/managers/hero/entities/interface/interface.ts +++ b/src/components/managers/hero/entities/interface/interface.ts @@ -36,6 +36,7 @@ export interface HeroProductEntityInterface { export interface FeatureArchiveProps { archive: { [key: number]: common_ArchiveFull[] }; + product: { [key: number]: common_Product[] } index: number; currentEntityIndex: number | null; open: boolean; From e7b5228f9cb26b27cdb3a1cfe17e7d77ca56f126 Mon Sep 17 00:00:00 2001 From: oak Date: Fri, 3 Jan 2025 19:08:37 +0300 Subject: [PATCH 5/6] store for fetch and filter products added --- .../products/listProducts/allProducts.tsx | 28 ++++---- .../filterProducts/filterProducts.tsx | 45 ++++++------ .../products/listProducts/listProducts.tsx | 5 +- .../useListProduct/useListProduct.ts | 50 -------------- src/lib/stores/store-types.ts | 22 +++++- src/lib/stores/store.ts | 68 ++++++++++++++++++- 6 files changed, 127 insertions(+), 91 deletions(-) delete mode 100644 src/components/managers/products/listProducts/useListProduct/useListProduct.ts diff --git a/src/components/managers/products/listProducts/allProducts.tsx b/src/components/managers/products/listProducts/allProducts.tsx index 30d311c2..b5b4243d 100644 --- a/src/components/managers/products/listProducts/allProducts.tsx +++ b/src/components/managers/products/listProducts/allProducts.tsx @@ -1,19 +1,18 @@ -import { Grid } from '@mui/material'; +import { Grid2 as Grid } from '@mui/material'; import { useNavigate } from '@tanstack/react-location'; import { deleteProductByID } from 'api/admin'; -import { GetProductsPagedRequest } from 'api/proto-http/admin'; +import { common_Product, GetProductsPagedRequest } from 'api/proto-http/admin'; import { ROUTES } from 'constants/routes'; -import { useSnackBarStore } from 'lib/stores/store'; +import { useProductStore, useSnackBarStore } from 'lib/stores/store'; import debounce from 'lodash/debounce'; import { FC, MouseEvent, useCallback, useEffect, useState } from 'react'; import { Filter } from './filterProducts/filterProducts'; import { ListProducts } from './listProducts'; -import useListProduct from './useListProduct/useListProduct'; export const AllProducts: FC = () => { const { showMessage } = useSnackBarStore(); - const { products, setProducts, filter, setFilter, isLoading, hasMore, fetchProducts } = - useListProduct(); + const { updateFilter, products, setProducts, isLoading, hasMore, filter, fetchProducts } = + useProductStore(); const [confirmDelete, setConfirmDelete] = useState(undefined); const [deletingProductId, setDeletingProductId] = useState(undefined); const navigate = useNavigate(); @@ -38,7 +37,9 @@ export const AllProducts: FC = () => { setDeletingProductId(productId); try { await deleteProductByID({ id: productId }); - setProducts((prevProducts) => prevProducts?.filter((product) => product.id !== productId)); + setProducts((prevProducts: common_Product[]) => + prevProducts?.filter((product) => product.id !== productId), + ); setTimeout(() => setDeletingProductId(undefined), 1000); } catch (error) { showMessage('the product cannot be removed', 'error'); @@ -50,7 +51,7 @@ export const AllProducts: FC = () => { const debouncedFetchProducts = useCallback( debounce((values: GetProductsPagedRequest) => { - fetchProducts(50, 0, values); + fetchProducts(50, 0); }, 500), [fetchProducts], ); @@ -62,7 +63,7 @@ export const AllProducts: FC = () => { !isLoading && hasMore ) { - fetchProducts(50, products.length, filter); + fetchProducts(50, products.length); } }; @@ -75,18 +76,17 @@ export const AllProducts: FC = () => { }, [filter, debouncedFetchProducts]); const handleFilterChange = (values: GetProductsPagedRequest) => { - setFilter(values); + updateFilter(values); debouncedFetchProducts(values); }; return ( - - + + - + void; } -export const Filter: FC = ({ filter, onFilterChange }) => { +export const Filter: FC = ({ onFilterChange }) => { + const { filter, updateFilter } = useProductStore(); const { dictionary } = useDictionaryStore(); const [isOpen, setIsOpen] = useState(true); const handleFieldChange = (setFieldValue: Function, fieldName: string, value: any) => { setFieldValue(fieldName, value); - const updatedFilter = { ...filter }; + let updatedFilter = {}; if (fieldName.includes('filterConditions')) { const keys = fieldName.split('.'); if (keys[1] === 'categoryIds') { - updatedFilter.filterConditions = { - ...filter.filterConditions, - categoryIds: Array.isArray(value) ? value : [value], - } as common_FilterConditions; + updatedFilter = { + filterConditions: { + categoryIds: Array.isArray(value) ? value : [value], + }, + }; } else if (keys[1] === 'sizesIds' && value.includes('')) { - updatedFilter.filterConditions = { - ...filter.filterConditions, - sizesIds: [], - } as common_FilterConditions; + updatedFilter = { + filterConditions: { + sizesIds: [], + }, + }; } else { - updatedFilter.filterConditions = { - ...filter.filterConditions, - [keys[1]]: value, - } as common_FilterConditions; + updatedFilter = { + filterConditions: { + [keys[1]]: value, + }, + }; } } else { - updatedFilter[fieldName as keyof GetProductsPagedRequest] = value; + updatedFilter = { + [fieldName]: value, + }; } - onFilterChange(updatedFilter); + updateFilter(updatedFilter); + onFilterChange({ ...filter, ...updatedFilter }); }; return ( diff --git a/src/components/managers/products/listProducts/listProducts.tsx b/src/components/managers/products/listProducts/listProducts.tsx index 86765de2..9bc9aace 100644 --- a/src/components/managers/products/listProducts/listProducts.tsx +++ b/src/components/managers/products/listProducts/listProducts.tsx @@ -1,14 +1,13 @@ import CheckIcon from '@mui/icons-material/Check'; import CloseIcon from '@mui/icons-material/Close'; import { Button, Grid2 as Grid, IconButton, Typography } from '@mui/material'; -import { common_Product } from 'api/proto-http/admin'; import { TruncateText } from 'components/common/truncateText'; import { isVideo } from 'features/utilitty/filterContentType'; +import { useProductStore } from 'lib/stores/store'; import React, { FC, useState } from 'react'; import styles from 'styles/paged.scss'; interface ProductProps { - products: common_Product[] | undefined; confirmDeleteProductId: number | undefined; deletingProductId: number | undefined; showHidden: boolean | undefined; @@ -18,7 +17,6 @@ interface ProductProps { } export const ListProducts: FC = ({ - products, confirmDeleteProductId, deletingProductId, showHidden, @@ -26,6 +24,7 @@ export const ListProducts: FC = ({ copy, deleteProduct, }) => { + const products = useProductStore((state) => state.products); const [hoveredProductId, setHoveredProductId] = useState(undefined); return ( diff --git a/src/components/managers/products/listProducts/useListProduct/useListProduct.ts b/src/components/managers/products/listProducts/useListProduct/useListProduct.ts deleted file mode 100644 index 7636e854..00000000 --- a/src/components/managers/products/listProducts/useListProduct/useListProduct.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { getProductsPaged } from 'api/admin'; -import { GetProductsPagedRequest, common_Product } from 'api/proto-http/admin'; -import React, { useCallback, useState } from 'react'; -import { defaultProductFilterSettings } from '../../../../../constants/initialFilterStates'; - - -const useListProduct = ( - initialLoading = false, - initialHasMore = true, -): { - products: common_Product[]; - setProducts: React.Dispatch>; - hasMore: boolean; - isLoading: boolean; - fetchProducts: ( - limit: number, - offset: number, - currentFilter: GetProductsPagedRequest, - ) => Promise; - filter: GetProductsPagedRequest; - setFilter: React.Dispatch>; -} => { - const [products, setProducts] = useState([]); - const [isLoading, setIsLoading] = useState(initialLoading); - const [hasMore, setHasMore] = useState(initialHasMore); - const [filter, setFilter] = useState(defaultProductFilterSettings); - - const fetchProducts = useCallback( - async (limit: number, offset: number, currentFilter: GetProductsPagedRequest) => { - setIsLoading(true); - const response = await getProductsPaged({ - limit, - offset, - sortFactors: currentFilter.sortFactors, - orderFactor: currentFilter?.orderFactor, - filterConditions: currentFilter?.filterConditions, - showHidden: currentFilter?.showHidden, - }); - const fetchedProducts = response.products || []; - setProducts((prev) => (offset === 0 ? fetchedProducts : [...prev, ...fetchedProducts])); - setIsLoading(false); - setHasMore(fetchedProducts.length === limit); - }, - [], - ); - - return { products, setProducts, isLoading, hasMore, fetchProducts, filter, setFilter }; -}; - -export default useListProduct; diff --git a/src/lib/stores/store-types.ts b/src/lib/stores/store-types.ts index 349afffb..437473d2 100644 --- a/src/lib/stores/store-types.ts +++ b/src/lib/stores/store-types.ts @@ -1,4 +1,4 @@ -import { common_Dictionary } from "api/proto-http/admin"; +import { common_Dictionary, common_FilterConditions, common_Product, GetProductsPagedRequest } from "api/proto-http/admin"; export interface DictionaryStore { dictionary: common_Dictionary | undefined; @@ -19,4 +19,22 @@ export interface SnackBarStore { showMessage: (message: string, severity: 'success' | 'error') => void; closeMessage: (id: number) => void; clearAll: () => void; -} \ No newline at end of file +} + +export interface ProductStore { + products: common_Product[]; + isLoading: boolean; + hasMore: boolean; + error: string | null; + filter: GetProductsPagedRequest; + setFilter: (filter: GetProductsPagedRequest) => void; + resetFilter: () => void; + updateFilter: (partialFilter: { + filterConditions?: Partial; + [key: string]: any; + }) => void; + fetchProducts: (limit: number, offset: number) => Promise; + setProducts: (products: common_Product[] | ((prev: common_Product[]) => common_Product[])) => void; + appendProducts: (newProducts: common_Product[]) => void; + clearProducts: () => void; +} diff --git a/src/lib/stores/store.ts b/src/lib/stores/store.ts index b4183c7b..a674c85c 100644 --- a/src/lib/stores/store.ts +++ b/src/lib/stores/store.ts @@ -1,7 +1,9 @@ -import { getDictionary } from "api/admin"; +import { getDictionary, getProductsPaged } from "api/admin"; +import { common_FilterConditions, GetProductsPagedRequest } from "api/proto-http/admin"; +import { defaultProductFilterSettings } from "constants/initialFilterStates"; import { create } from "zustand"; -import { DictionaryStore, SnackBarStore } from "./store-types"; +import { DictionaryStore, ProductStore, SnackBarStore } from "./store-types"; export const useDictionaryStore = create((set, get) => ({ dictionary: undefined, @@ -39,4 +41,64 @@ export const useSnackBarStore = create((set) => ({ clearAll: () => { set({ alerts: [] }); } -})); \ No newline at end of file +})); + +export const useProductStore = create((set, get) => ({ + products: [], + isLoading: false, + hasMore: false, + error: null, + filter: defaultProductFilterSettings, + setFilter: (filter: GetProductsPagedRequest) => { + set({ filter }); + }, + resetFilter: () => { + set({ filter: defaultProductFilterSettings }); + }, + updateFilter: (partialFilter) => { + set((state) => ({ + filter: { + ...state.filter, + ...partialFilter, + filterConditions: { + ...state.filter.filterConditions, + ...partialFilter.filterConditions, + } as common_FilterConditions, + }, + })); + }, + fetchProducts: async (limit: number, offset: number) => { + const { filter } = get(); + set({ isLoading: true, error: null }); + + try { + const response = await getProductsPaged({ + ...filter, + limit, + offset, + }) + const fetchedProducts = response.products || []; + + if (offset === 0) { + set({ products: fetchedProducts }) + } else { + set((state) => ({ + products: [...state.products, ...fetchedProducts], + })) + } + + set({ hasMore: fetchedProducts.length === limit, isLoading: false }) + } catch (error) { + set({ error: 'Failed to fetch products', isLoading: false }) + } + }, + setProducts: (products) => set((state) => ({ + products: typeof products === 'function' ? products(state.products) : products + })), + appendProducts: (newProducts) => set((state) => ({ + products: [...state.products, ...newProducts] + })), + clearProducts: () => set({ products: [] }) +})) + + From cc8132b35cfe1b18f9e43498d22f18648e426b79 Mon Sep 17 00:00:00 2001 From: oak Date: Fri, 3 Jan 2025 19:57:52 +0300 Subject: [PATCH 6/6] move to product page by page from hero featured archive done --- .../featured-archive/featured-archive.tsx | 41 ++++++++++++++++--- .../products/listProducts/allProducts.tsx | 36 ++++++++++------ src/lib/stores/store-types.ts | 2 +- src/lib/stores/store.ts | 4 +- 4 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/components/managers/hero/entities/featured-archive/featured-archive.tsx b/src/components/managers/hero/entities/featured-archive/featured-archive.tsx index 96bb7b51..63638c83 100644 --- a/src/components/managers/hero/entities/featured-archive/featured-archive.tsx +++ b/src/components/managers/hero/entities/featured-archive/featured-archive.tsx @@ -1,15 +1,17 @@ import { Button, Grid2 as Grid, TextField, Typography } from '@mui/material'; +import { useNavigate } from '@tanstack/react-location'; +import { common_HeroFullInsert } from 'api/proto-http/admin'; import { CopyToClipboard } from 'components/common/copyToClipboard'; import { TruncateText } from 'components/common/truncateText'; -import { Field } from 'formik'; +import { defaultProductFilterSettings } from 'constants/initialFilterStates'; +import { ROUTES } from 'constants/routes'; +import { Field, useFormikContext } from 'formik'; import styles from 'styles/archiveList.scss'; -import { HeroProductTable } from '../featured-products-(tags)/heroProductsTable'; import { FeatureArchiveProps } from '../interface/interface'; import { ArchivePicker } from './archive-picker'; export function FeaturedArchive({ archive, - product, index, currentEntityIndex, open, @@ -17,6 +19,28 @@ export function FeaturedArchive({ handleSaveArchiveSelection, handleOpenArchiveSelection, }: FeatureArchiveProps) { + const navigate = useNavigate(); + const { values } = useFormikContext(); + + const handleTagClick = () => { + const tag = values.entities?.[index]?.featuredArchive?.tag; + if (tag) { + const filterSettings = { + ...defaultProductFilterSettings, + filterConditions: { + ...defaultProductFilterSettings.filterConditions, + byTag: tag, + }, + }; + navigate({ + to: `${ROUTES.product}`, + search: (old) => ({ + ...old, + filter: filterSettings, + }), + }); + } + }; return ( @@ -112,16 +136,23 @@ export function FeaturedArchive({ name={`entities.${index}.featuredArchive.tag`} label='tag' fullWidth + InputProps={{ + endAdornment: ( + + ), + }} /> - + {/* - + */} ); } diff --git a/src/components/managers/products/listProducts/allProducts.tsx b/src/components/managers/products/listProducts/allProducts.tsx index b5b4243d..759111cd 100644 --- a/src/components/managers/products/listProducts/allProducts.tsx +++ b/src/components/managers/products/listProducts/allProducts.tsx @@ -1,5 +1,5 @@ import { Grid2 as Grid } from '@mui/material'; -import { useNavigate } from '@tanstack/react-location'; +import { useNavigate, useSearch } from '@tanstack/react-location'; import { deleteProductByID } from 'api/admin'; import { common_Product, GetProductsPagedRequest } from 'api/proto-http/admin'; import { ROUTES } from 'constants/routes'; @@ -16,6 +16,29 @@ export const AllProducts: FC = () => { const [confirmDelete, setConfirmDelete] = useState(undefined); const [deletingProductId, setDeletingProductId] = useState(undefined); const navigate = useNavigate(); + const search = useSearch(); + + const debouncedFetchProducts = useCallback( + debounce((values: GetProductsPagedRequest) => { + fetchProducts(50, 0, values); + }, 500), + [fetchProducts], + ); + + useEffect(() => { + if (search.filter) { + try { + const filterFromUrl = + typeof search.filter === 'string' ? JSON.parse(search.filter) : search.filter; + updateFilter(filterFromUrl); + fetchProducts(50, 0, filterFromUrl); + } catch (error) { + console.error('Failed to parse filter from URL:', error); + } + } else { + debouncedFetchProducts(filter); + } + }, [search.filter]); const handleProductClick = (id: number | undefined) => { navigate({ to: `${ROUTES.product}/${id}`, replace: true }); @@ -49,13 +72,6 @@ export const AllProducts: FC = () => { } }; - const debouncedFetchProducts = useCallback( - debounce((values: GetProductsPagedRequest) => { - fetchProducts(50, 0); - }, 500), - [fetchProducts], - ); - useEffect(() => { const handleScroll = () => { if ( @@ -71,10 +87,6 @@ export const AllProducts: FC = () => { return () => window.removeEventListener('scroll', handleScroll); }, [isLoading, hasMore, products.length, fetchProducts]); - useEffect(() => { - debouncedFetchProducts(filter); - }, [filter, debouncedFetchProducts]); - const handleFilterChange = (values: GetProductsPagedRequest) => { updateFilter(values); debouncedFetchProducts(values); diff --git a/src/lib/stores/store-types.ts b/src/lib/stores/store-types.ts index 437473d2..70f18675 100644 --- a/src/lib/stores/store-types.ts +++ b/src/lib/stores/store-types.ts @@ -33,7 +33,7 @@ export interface ProductStore { filterConditions?: Partial; [key: string]: any; }) => void; - fetchProducts: (limit: number, offset: number) => Promise; + fetchProducts: (limit: number, offset: number, filterValues?: GetProductsPagedRequest) => Promise; setProducts: (products: common_Product[] | ((prev: common_Product[]) => common_Product[])) => void; appendProducts: (newProducts: common_Product[]) => void; clearProducts: () => void; diff --git a/src/lib/stores/store.ts b/src/lib/stores/store.ts index a674c85c..ef9efca3 100644 --- a/src/lib/stores/store.ts +++ b/src/lib/stores/store.ts @@ -67,13 +67,13 @@ export const useProductStore = create((set, get) => ({ }, })); }, - fetchProducts: async (limit: number, offset: number) => { + fetchProducts: async (limit: number, offset: number, filterValues?: GetProductsPagedRequest) => { const { filter } = get(); set({ isLoading: true, error: null }); try { const response = await getProductsPaged({ - ...filter, + ...(filterValues || filter), limit, offset, })