first commit
This commit is contained in:
parent
3986fd953b
commit
4efeaa9dfe
40
package-lock.json
generated
40
package-lock.json
generated
@ -31,6 +31,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.522.0",
|
||||
"mantine-datatable": "^1.7.17",
|
||||
"nuqs": "^2.4.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.58.1",
|
||||
@ -16067,6 +16068,12 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
@ -16241,6 +16248,39 @@
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/nuqs": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.4.3.tgz",
|
||||
"integrity": "sha512-BgtlYpvRwLYiJuWzxt34q2bXu/AIS66sLU1QePIMr2LWkb+XH0vKXdbLSgn9t6p7QKzwI7f38rX3Wl9llTXQ8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mitt": "^3.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/franky47"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@remix-run/react": ">=2",
|
||||
"next": ">=14.2.0",
|
||||
"react": ">=18.2.0 || ^19.0.0-0",
|
||||
"react-router": "^6 || ^7",
|
||||
"react-router-dom": "^6 || ^7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@remix-run/react": {
|
||||
"optional": true
|
||||
},
|
||||
"next": {
|
||||
"optional": true
|
||||
},
|
||||
"react-router": {
|
||||
"optional": true
|
||||
},
|
||||
"react-router-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nwsapi": {
|
||||
"version": "2.2.13",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz",
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.522.0",
|
||||
"mantine-datatable": "^1.7.17",
|
||||
"nuqs": "^2.4.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.58.1",
|
||||
|
||||
@ -5,9 +5,9 @@ import { IRootState } from '../../store';
|
||||
import { toggleSidebar } from '../../store/themeConfigSlice';
|
||||
import Footer from './Footer';
|
||||
import Header from './Header';
|
||||
import Setting from './Setting';
|
||||
import Sidebar from './Sidebar';
|
||||
import Portals from '../../components/Portals';
|
||||
import IconLoader from '../Icon/IconLoader';
|
||||
|
||||
const DefaultLayout = ({ children }: PropsWithChildren) => {
|
||||
const themeConfig = useSelector((state: IRootState) => state.themeConfig);
|
||||
@ -89,7 +89,13 @@ const DefaultLayout = ({ children }: PropsWithChildren) => {
|
||||
{/* END TOP NAVBAR */}
|
||||
|
||||
{/* BEGIN CONTENT AREA */}
|
||||
<Suspense>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex h-screen w-screen items-center justify-center">
|
||||
<IconLoader className="animate-spin w-16 h-16" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={`${themeConfig.animation} p-6 animate__animated`}>{children}</div>
|
||||
</Suspense>
|
||||
{/* END CONTENT AREA */}
|
||||
|
||||
@ -14,8 +14,6 @@ import IconLaptop from '../Icon/IconLaptop';
|
||||
import IconInfoCircle from '../Icon/IconInfoCircle';
|
||||
import IconBellBing from '../Icon/IconBellBing';
|
||||
import IconUser from '../Icon/IconUser';
|
||||
import IconMail from '../Icon/IconMail';
|
||||
import IconLockDots from '../Icon/IconLockDots';
|
||||
import IconLogout from '../Icon/IconLogout';
|
||||
import IconMenuDashboard from '../Icon/Menu/IconMenuDashboard';
|
||||
import IconCaretDown from '../Icon/IconCaretDown';
|
||||
@ -27,6 +25,7 @@ import IconMenuForms from '../Icon/Menu/IconMenuForms';
|
||||
import IconMenuPages from '../Icon/Menu/IconMenuPages';
|
||||
import IconMenuMore from '../Icon/Menu/IconMenuMore';
|
||||
import { useLogout, useUser } from '../../lib/auth';
|
||||
import { LogoutModal } from '../../features/auth/components/LogoutModal';
|
||||
|
||||
const Header = () => {
|
||||
const location = useLocation();
|
||||
@ -92,11 +91,12 @@ const Header = () => {
|
||||
const [flag, setFlag] = useState(themeConfig.locale);
|
||||
|
||||
const user = useUser();
|
||||
const logout = useLogout();
|
||||
const [isLogoutModalOpen, setIsLogoutModalOpen] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className={`z-40 ${themeConfig.semidark && themeConfig.menu === 'horizontal' ? 'dark' : ''}`}>
|
||||
<div className="shadow-sm">
|
||||
<div className="relative bg-white flex w-full items-center px-5 py-2.5 dark:bg-black">
|
||||
@ -290,7 +290,7 @@ const Header = () => {
|
||||
</Link>
|
||||
</li>
|
||||
<li className="border-t border-white-light dark:border-white-light/10">
|
||||
<button onClick={async () => await logout.mutateAsync({})} className="text-danger !py-3" disabled={logout.isPending}>
|
||||
<button onClick={() => setIsLogoutModalOpen(true)} className="text-danger !py-3">
|
||||
<IconLogout className="w-4.5 h-4.5 ltr:mr-2 rtl:ml-2 rotate-90 shrink-0" />
|
||||
Sign Out
|
||||
</button>
|
||||
@ -821,6 +821,8 @@ const Header = () => {
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
<LogoutModal isOpen={isLogoutModalOpen} onClose={() => setIsLogoutModalOpen(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
65
src/features/auth/components/LogoutModal.tsx
Normal file
65
src/features/auth/components/LogoutModal.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { Fragment } from 'react';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { DialogPanel, TransitionChild } from '@headlessui/react';
|
||||
import { useLogout } from '../../../lib/auth';
|
||||
import IconX from '../../../components/Icon/IconX';
|
||||
import IconLoader from '../../../components/Icon/IconLoader';
|
||||
|
||||
interface LogoutModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const LogoutModal = ({ isOpen, onClose }: LogoutModalProps) => {
|
||||
const logout = useLogout();
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout.mutateAsync({});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={onClose}>
|
||||
{/* BACKDROP */}
|
||||
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0">
|
||||
<div className="fixed inset-0 bg-black/60" />
|
||||
</TransitionChild>
|
||||
|
||||
{/* PANEL */}
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel className="w-full max-w-md transform overflow-hidden rounded-xl bg-white p-6 text-center align-middle shadow-xl transition-all dark:bg-[#121c2c]">
|
||||
{/* CLOSE */}
|
||||
<button type="button" onClick={onClose} className="absolute right-4 top-4 text-gray-400 hover:text-gray-800 dark:hover:text-white">
|
||||
<IconX />
|
||||
</button>
|
||||
|
||||
<Dialog.Title as="h3" className="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
Confirm Logout
|
||||
</Dialog.Title>
|
||||
<div className="mt-4 text-sm text-gray-600 dark:text-gray-300">Are you sure you want to logout?</div>
|
||||
|
||||
<div className="mt-6 flex justify-center gap-4">
|
||||
<button type="button" onClick={onClose} className="btn btn-outline-primary">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" onClick={handleLogout} className="btn btn-danger disabled:opacity-50" disabled={logout.isPending}>
|
||||
{logout.isPending ? <IconLoader /> : 'Logout'}
|
||||
</button>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
@ -22,7 +22,7 @@ export const useCreateProductCategory = ({ mutationConfig }: UseCreateProductCat
|
||||
mutationFn: createProductCategory,
|
||||
onSuccess: (...args) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProductCategoriesQueryOptions().queryKey,
|
||||
queryKey: [getProductCategoriesQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(...args);
|
||||
},
|
||||
|
||||
@ -21,7 +21,7 @@ export const useDeleteProductCategory = ({ mutationConfig }: UseDeleteProductCat
|
||||
mutationFn: deleteProductCategory,
|
||||
onSuccess: (...args) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProductCategoriesQueryOptions({}).queryKey,
|
||||
queryKey: [getProductCategoriesQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(...args);
|
||||
},
|
||||
|
||||
@ -4,21 +4,21 @@ import { api } from '../../../lib/apiClient';
|
||||
import { ProductCategoryResponse } from '../types/api';
|
||||
|
||||
type ProductCategoriesQueryParams = {
|
||||
q?: string;
|
||||
search?: string | null;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export const getProductCategories = ({ q, page, limit }: ProductCategoriesQueryParams = {}): Promise<ProductCategoryResponse> => {
|
||||
export const getProductCategories = ({ search, page, limit }: ProductCategoriesQueryParams = {}): Promise<ProductCategoryResponse> => {
|
||||
return api.get('/inventory/categories', {
|
||||
params: { q, page, limit },
|
||||
params: { search, page, limit },
|
||||
});
|
||||
};
|
||||
|
||||
export const getProductCategoriesQueryOptions = ({ q, page, limit }: ProductCategoriesQueryParams = {}) => {
|
||||
export const getProductCategoriesQueryOptions = ({ search, page, limit }: ProductCategoriesQueryParams = {}) => {
|
||||
return queryOptions({
|
||||
queryKey: ['productCategories', { q, page, limit }],
|
||||
queryFn: () => getProductCategories({ q, page, limit }),
|
||||
queryKey: ['productCategories', { search, page, limit }],
|
||||
queryFn: () => getProductCategories({ search, page, limit }),
|
||||
});
|
||||
};
|
||||
|
||||
@ -26,9 +26,9 @@ type UseProductCategoriesOptions = {
|
||||
queryConfig?: QueryConfig<typeof getProductCategoriesQueryOptions>;
|
||||
} & ProductCategoriesQueryParams;
|
||||
|
||||
export const useProductCategories = ({ q, page, limit, queryConfig }: UseProductCategoriesOptions = {}) => {
|
||||
export const useProductCategories = ({ search, page, limit, queryConfig }: UseProductCategoriesOptions = {}) => {
|
||||
return useQuery({
|
||||
...getProductCategoriesQueryOptions({ q, page, limit }),
|
||||
...getProductCategoriesQueryOptions({ search, page, limit }),
|
||||
...queryConfig,
|
||||
});
|
||||
};
|
||||
|
||||
@ -22,7 +22,7 @@ export const useUpdateProductCategory = ({ mutationConfig }: UseUpdateProductCat
|
||||
mutationFn: updateProductCategory,
|
||||
onSuccess: (data, ...args) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProductCategoriesQueryOptions().queryKey,
|
||||
queryKey: [getProductCategoriesQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(data, ...args);
|
||||
},
|
||||
|
||||
@ -8,6 +8,8 @@ import { ProductCategory } from '../types/api';
|
||||
import { EditProductCategoryModal } from './EditProductCategoryModal';
|
||||
import { DeleteProductCategoryModal } from './DeleteProductCategoryModal';
|
||||
import { DetailProductCategoryModal } from './DetailProductCategoryModal';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { useDebounce } from '../../../hooks/useDebounce';
|
||||
|
||||
const PAGE_SIZES = [10, 20, 30, 50];
|
||||
|
||||
@ -17,7 +19,10 @@ export const ProductCategoriesList = () => {
|
||||
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||||
const [selectedCategory, setSelectedCategory] = useState<ProductCategory | null>(null);
|
||||
|
||||
const categoryQuery = useProductCategories();
|
||||
const [searchParam, setSearchParam] = useQueryState('search');
|
||||
const debouncedSearchParam = useDebounce(searchParam, 500);
|
||||
|
||||
const categoryQuery = useProductCategories({ search: debouncedSearchParam });
|
||||
const categories = categoryQuery.data ?? [];
|
||||
|
||||
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(categories, {
|
||||
@ -25,10 +30,6 @@ export const ProductCategoriesList = () => {
|
||||
direction: 'desc',
|
||||
});
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const filteredRecords = paginatedRecords.filter((category) => category.name.toLowerCase().includes(search.toLowerCase()));
|
||||
|
||||
const onDelete = (category: ProductCategory) => {
|
||||
setSelectedCategory(category);
|
||||
setIsDeleteModalOpen(true);
|
||||
@ -47,12 +48,12 @@ export const ProductCategoriesList = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
||||
<ProductCategoryListHeader search={search} setSearch={setSearch} />
|
||||
<ProductCategoryListHeader search={searchParam} setSearch={setSearchParam} />
|
||||
|
||||
<div className="datatables pagination-padding px-5">
|
||||
<DataTable
|
||||
className="whitespace-nowrap table-hover"
|
||||
records={filteredRecords}
|
||||
records={paginatedRecords}
|
||||
columns={productCategoryColumns({ onEdit, onView, onDelete })}
|
||||
highlightOnHover
|
||||
minHeight={200}
|
||||
|
||||
@ -3,7 +3,7 @@ import IconPlus from '../../../components/Icon/IconPlus';
|
||||
import { useState } from 'react';
|
||||
import { CreateProductCategoryModal } from './CreateProductCategoryModal';
|
||||
|
||||
export const ProductCategoryListHeader = ({ search, setSearch }: { search: string; setSearch: (v: string) => void }) => {
|
||||
export const ProductCategoryListHeader = ({ search, setSearch }: { search: string | null; setSearch: (v: string | null) => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isOpenModalCreateCategory, setIsOpenModalCreateCategory] = useState(false);
|
||||
|
||||
@ -11,7 +11,7 @@ export const ProductCategoryListHeader = ({ search, setSearch }: { search: strin
|
||||
<>
|
||||
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
||||
<div className="ltr:mr-auto rtl:ml-auto">
|
||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search || ''} onChange={(e) => setSearch(e.target.value || null)} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateCategory(true)}>
|
||||
|
||||
@ -22,7 +22,7 @@ export const useCreateProductCollection = ({ mutationConfig }: UseCreateProductC
|
||||
mutationFn: createProductCollection,
|
||||
onSuccess: (...args) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProductCollectionsQueryOptions().queryKey,
|
||||
queryKey: [getProductCollectionsQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(...args);
|
||||
},
|
||||
|
||||
@ -20,7 +20,7 @@ export const useDeleteProductCollection = ({ mutationConfig }: UseDeleteProductC
|
||||
mutationFn: deleteProductCollection,
|
||||
onSuccess: (...args) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProductCollectionsQueryOptions().queryKey,
|
||||
queryKey: [getProductCollectionsQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(...args);
|
||||
},
|
||||
|
||||
@ -4,21 +4,21 @@ import { api } from '../../../lib/apiClient';
|
||||
import { ProductCollectionResponse } from '../types/api';
|
||||
|
||||
type ProductCollectionsQueryParams = {
|
||||
q?: string;
|
||||
search?: string | null;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export const getProductCollections = ({ q, page, limit }: ProductCollectionsQueryParams = {}): Promise<ProductCollectionResponse> => {
|
||||
export const getProductCollections = ({ search, page, limit }: ProductCollectionsQueryParams = {}): Promise<ProductCollectionResponse> => {
|
||||
return api.get('/inventory/collections', {
|
||||
params: { q, page, limit },
|
||||
params: { search, page, limit },
|
||||
});
|
||||
};
|
||||
|
||||
export const getProductCollectionsQueryOptions = ({ q, page, limit }: ProductCollectionsQueryParams = {}) => {
|
||||
export const getProductCollectionsQueryOptions = ({ search, page, limit }: ProductCollectionsQueryParams = {}) => {
|
||||
return queryOptions({
|
||||
queryKey: ['productCollections', { q, page, limit }],
|
||||
queryFn: () => getProductCollections({ q, page, limit }),
|
||||
queryKey: ['productCollections', { search, page, limit }],
|
||||
queryFn: () => getProductCollections({ search, page, limit }),
|
||||
});
|
||||
};
|
||||
|
||||
@ -26,9 +26,9 @@ type UseProductCollectionsOptions = {
|
||||
queryConfig?: QueryConfig<typeof getProductCollectionsQueryOptions>;
|
||||
} & ProductCollectionsQueryParams;
|
||||
|
||||
export const useProductCollections = ({ q, page, limit, queryConfig }: UseProductCollectionsOptions = {}) => {
|
||||
export const useProductCollections = ({ search, page, limit, queryConfig }: UseProductCollectionsOptions = {}) => {
|
||||
return useQuery({
|
||||
...getProductCollectionsQueryOptions({ q, page, limit }),
|
||||
...getProductCollectionsQueryOptions({ search, page, limit }),
|
||||
...queryConfig,
|
||||
});
|
||||
};
|
||||
|
||||
@ -22,7 +22,7 @@ export const useUpdateProductCollection = ({ mutationConfig }: UseUpdateProductC
|
||||
mutationFn: updateProductCollection,
|
||||
onSuccess: (data, ...args) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProductCollectionsQueryOptions().queryKey,
|
||||
queryKey: [getProductCollectionsQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(data, ...args);
|
||||
},
|
||||
|
||||
@ -3,7 +3,7 @@ import IconPlus from '../../../components/Icon/IconPlus';
|
||||
import { useState } from 'react';
|
||||
import { CreateProductCollectionModal } from './CreateProductCollectionModal';
|
||||
|
||||
export const ProductCollectionListHeader = ({ search, setSearch }: { search: string; setSearch: (v: string) => void }) => {
|
||||
export const ProductCollectionListHeader = ({ search, setSearch }: { search: string | null; setSearch: (v: string | null) => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isOpenModalCreateCollection, setIsOpenModalCreateCollection] = useState(false);
|
||||
|
||||
@ -11,7 +11,7 @@ export const ProductCollectionListHeader = ({ search, setSearch }: { search: str
|
||||
<>
|
||||
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
||||
<div className="ltr:mr-auto rtl:ml-auto">
|
||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search || ''} onChange={(e) => setSearch(e.target.value || null)} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateCollection(true)}>
|
||||
|
||||
@ -8,6 +8,8 @@ import { ProductCollection } from '../types/api';
|
||||
import { DeleteProductCollectionModal } from './DeleteProductCollectionModal';
|
||||
import { EditProductCollectionModal } from './EditProductCollectionModal';
|
||||
import { DetailProductCollectionModal } from './DetailProductCollectionModal';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { useDebounce } from '../../../hooks/useDebounce';
|
||||
|
||||
const PAGE_SIZES = [10, 20, 30, 50];
|
||||
|
||||
@ -17,7 +19,10 @@ export const ProductCollectionsList = () => {
|
||||
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||||
const [selectedCollection, setSelectedCollection] = useState<ProductCollection | null>(null);
|
||||
|
||||
const collectionQuery = useProductCollections();
|
||||
const [searchParam, setSearchParam] = useQueryState('search');
|
||||
const debouncedSearchParam = useDebounce(searchParam, 500);
|
||||
|
||||
const collectionQuery = useProductCollections({ search: debouncedSearchParam });
|
||||
const collections = collectionQuery.data ?? [];
|
||||
|
||||
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(collections, {
|
||||
@ -25,10 +30,6 @@ export const ProductCollectionsList = () => {
|
||||
direction: 'desc',
|
||||
});
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const filteredRecords = paginatedRecords.filter((collection) => collection.name.toLowerCase().includes(search.toLowerCase()));
|
||||
|
||||
const onDelete = (collection: ProductCollection) => {
|
||||
setSelectedCollection(collection);
|
||||
setIsDeleteModalOpen(true);
|
||||
@ -47,12 +48,12 @@ export const ProductCollectionsList = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
||||
<ProductCollectionListHeader search={search} setSearch={setSearch} />
|
||||
<ProductCollectionListHeader search={searchParam} setSearch={setSearchParam} />
|
||||
|
||||
<div className="datatables pagination-padding px-5">
|
||||
<DataTable
|
||||
className="whitespace-nowrap table-hover"
|
||||
records={filteredRecords}
|
||||
records={paginatedRecords}
|
||||
columns={productCollectionColumns({ onEdit, onView, onDelete })}
|
||||
highlightOnHover
|
||||
minHeight={200}
|
||||
|
||||
@ -22,7 +22,7 @@ export const useCreateProductColour = ({ mutationConfig }: UseCreateProductColou
|
||||
mutationFn: createProductColour,
|
||||
onSuccess: (...args) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProductColoursQueryOptions().queryKey,
|
||||
queryKey: [getProductColoursQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(...args);
|
||||
},
|
||||
|
||||
@ -20,7 +20,7 @@ export const useDeleteProductColour = ({ mutationConfig }: UseDeleteProductColou
|
||||
mutationFn: deleteProductColour,
|
||||
onSuccess: (...args) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProductColoursQueryOptions().queryKey,
|
||||
queryKey: [getProductColoursQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(...args);
|
||||
},
|
||||
|
||||
@ -4,21 +4,21 @@ import { api } from '../../../lib/apiClient';
|
||||
import { ProductColourResponse } from '../types/api';
|
||||
|
||||
type ProductColoursQueryParams = {
|
||||
q?: string;
|
||||
search?: string | null;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export const getProductColours = ({ q, page, limit }: ProductColoursQueryParams = {}): Promise<ProductColourResponse> => {
|
||||
export const getProductColours = ({ search, page, limit }: ProductColoursQueryParams = {}): Promise<ProductColourResponse> => {
|
||||
return api.get('/inventory/colours', {
|
||||
params: { q, page, limit },
|
||||
params: { search, page, limit },
|
||||
});
|
||||
};
|
||||
|
||||
export const getProductColoursQueryOptions = ({ q, page, limit }: ProductColoursQueryParams = {}) => {
|
||||
export const getProductColoursQueryOptions = ({ search, page, limit }: ProductColoursQueryParams = {}) => {
|
||||
return queryOptions({
|
||||
queryKey: ['productColours', { q, page, limit }],
|
||||
queryFn: () => getProductColours({ q, page, limit }),
|
||||
queryKey: ['productColours', { search, page, limit }],
|
||||
queryFn: () => getProductColours({ search, page, limit }),
|
||||
});
|
||||
};
|
||||
|
||||
@ -26,9 +26,9 @@ type UseProductColoursOptions = {
|
||||
queryConfig?: QueryConfig<typeof getProductColoursQueryOptions>;
|
||||
} & ProductColoursQueryParams;
|
||||
|
||||
export const useProductColours = ({ q, page, limit, queryConfig }: UseProductColoursOptions = {}) => {
|
||||
export const useProductColours = ({ search, page, limit, queryConfig }: UseProductColoursOptions = {}) => {
|
||||
return useQuery({
|
||||
...getProductColoursQueryOptions({ q, page, limit }),
|
||||
...getProductColoursQueryOptions({ search, page, limit }),
|
||||
...queryConfig,
|
||||
});
|
||||
};
|
||||
|
||||
@ -22,7 +22,7 @@ export const useUpdateProductColour = ({ mutationConfig }: UseUpdateProductColou
|
||||
mutationFn: updateProductColour,
|
||||
onSuccess: (data, ...args) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProductColoursQueryOptions().queryKey,
|
||||
queryKey: [getProductColoursQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(data, ...args);
|
||||
},
|
||||
|
||||
@ -3,7 +3,7 @@ import IconPlus from '../../../components/Icon/IconPlus';
|
||||
import { useState } from 'react';
|
||||
import { CreateProductColourModal } from './CreateProductColourModal';
|
||||
|
||||
export const ProductColourListHeader = ({ search, setSearch }: { search: string; setSearch: (v: string) => void }) => {
|
||||
export const ProductColourListHeader = ({ search, setSearch }: { search: string | null; setSearch: (v: string | null) => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isOpenModalCreateColour, setIsOpenModalCreateColour] = useState(false);
|
||||
|
||||
@ -11,7 +11,7 @@ export const ProductColourListHeader = ({ search, setSearch }: { search: string;
|
||||
<>
|
||||
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
||||
<div className="ltr:mr-auto rtl:ml-auto">
|
||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search || ''} onChange={(e) => setSearch(e.target.value || null)} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateColour(true)}>
|
||||
|
||||
@ -8,6 +8,8 @@ import { ProductColour } from '../types/api';
|
||||
import { DeleteProductColourModal } from './DeleteProductColourModal';
|
||||
import { EditProductColourModal } from './EditProductColourModal';
|
||||
import { DetailProductColourModal } from './DetailProductColourModal';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { useDebounce } from '../../../hooks/useDebounce';
|
||||
|
||||
const PAGE_SIZES = [10, 20, 30, 50];
|
||||
|
||||
@ -17,7 +19,10 @@ export const ProductColoursList = () => {
|
||||
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||||
const [selectedColour, setSelectedColour] = useState<ProductColour | null>(null);
|
||||
|
||||
const colourQuery = useProductColours();
|
||||
const [searchParam, setSearchParam] = useQueryState('search');
|
||||
const debouncedSearchParam = useDebounce(searchParam, 500);
|
||||
|
||||
const colourQuery = useProductColours({ search: debouncedSearchParam });
|
||||
const colours = colourQuery.data ?? [];
|
||||
|
||||
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(colours, {
|
||||
@ -25,10 +30,6 @@ export const ProductColoursList = () => {
|
||||
direction: 'desc',
|
||||
});
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const filteredRecords = paginatedRecords.filter((colour) => colour.name.toLowerCase().includes(search.toLowerCase()));
|
||||
|
||||
const onDelete = (colour: ProductColour) => {
|
||||
setSelectedColour(colour);
|
||||
setIsDeleteModalOpen(true);
|
||||
@ -47,12 +48,12 @@ export const ProductColoursList = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
||||
<ProductColourListHeader search={search} setSearch={setSearch} />
|
||||
<ProductColourListHeader search={searchParam} setSearch={setSearchParam} />
|
||||
|
||||
<div className="datatables pagination-padding px-5">
|
||||
<DataTable
|
||||
className="whitespace-nowrap table-hover"
|
||||
records={filteredRecords}
|
||||
records={paginatedRecords}
|
||||
columns={productColourColumns({ onEdit, onView, onDelete })}
|
||||
highlightOnHover
|
||||
minHeight={200}
|
||||
|
||||
@ -22,7 +22,7 @@ export const useCreateProductSize = ({ mutationConfig }: UseCreateProductSizeOpt
|
||||
mutationFn: createProductSize,
|
||||
onSuccess: (...args) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProductSizesQueryOptions().queryKey,
|
||||
queryKey: [getProductSizesQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(...args);
|
||||
},
|
||||
|
||||
@ -20,7 +20,7 @@ export const useDeleteProductSize = ({ mutationConfig }: UseDeleteProductSizeOpt
|
||||
mutationFn: deleteProductSize,
|
||||
onSuccess: (...args) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProductSizesQueryOptions().queryKey,
|
||||
queryKey: [getProductSizesQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(...args);
|
||||
},
|
||||
|
||||
@ -4,21 +4,21 @@ import { api } from '../../../lib/apiClient';
|
||||
import { ProductSizeResponse } from '../types/api';
|
||||
|
||||
type ProductSizesQueryParams = {
|
||||
q?: string;
|
||||
search?: string | null;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export const getProductSizes = ({ q, page, limit }: ProductSizesQueryParams = {}): Promise<ProductSizeResponse> => {
|
||||
export const getProductSizes = ({ search, page, limit }: ProductSizesQueryParams = {}): Promise<ProductSizeResponse> => {
|
||||
return api.get('/inventory/sizes', {
|
||||
params: { q, page, limit },
|
||||
params: { search, page, limit },
|
||||
});
|
||||
};
|
||||
|
||||
export const getProductSizesQueryOptions = ({ q, page, limit }: ProductSizesQueryParams = {}) => {
|
||||
export const getProductSizesQueryOptions = ({ search, page, limit }: ProductSizesQueryParams = {}) => {
|
||||
return queryOptions({
|
||||
queryKey: ['productSizes', { q, page, limit }],
|
||||
queryFn: () => getProductSizes({ q, page, limit }),
|
||||
queryKey: ['productSizes', { search, page, limit }],
|
||||
queryFn: () => getProductSizes({ search, page, limit }),
|
||||
});
|
||||
};
|
||||
|
||||
@ -26,9 +26,9 @@ type UseProductSizesOptions = {
|
||||
queryConfig?: QueryConfig<typeof getProductSizesQueryOptions>;
|
||||
} & ProductSizesQueryParams;
|
||||
|
||||
export const useProductSizes = ({ q, page, limit, queryConfig }: UseProductSizesOptions = {}) => {
|
||||
export const useProductSizes = ({ search, page, limit, queryConfig }: UseProductSizesOptions = {}) => {
|
||||
return useQuery({
|
||||
...getProductSizesQueryOptions({ q, page, limit }),
|
||||
...getProductSizesQueryOptions({ search, page, limit }),
|
||||
...queryConfig,
|
||||
});
|
||||
};
|
||||
|
||||
@ -22,7 +22,7 @@ export const useUpdateProductSize = ({ mutationConfig }: UseUpdateProductSizeOpt
|
||||
mutationFn: updateProductSize,
|
||||
onSuccess: (data, ...args) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProductSizesQueryOptions().queryKey,
|
||||
queryKey: [getProductSizesQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(data, ...args);
|
||||
},
|
||||
|
||||
@ -3,7 +3,7 @@ import IconPlus from '../../../components/Icon/IconPlus';
|
||||
import { useState } from 'react';
|
||||
import { CreateProductSizeModal } from './CreateProductSizeModal';
|
||||
|
||||
export const ProductSizeListHeader = ({ search, setSearch }: { search: string; setSearch: (v: string) => void }) => {
|
||||
export const ProductSizeListHeader = ({ search, setSearch }: { search: string | null; setSearch: (v: string | null) => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isOpenModalCreateSize, setIsOpenModalCreateSize] = useState(false);
|
||||
|
||||
@ -11,7 +11,7 @@ export const ProductSizeListHeader = ({ search, setSearch }: { search: string; s
|
||||
<>
|
||||
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
||||
<div className="ltr:mr-auto rtl:ml-auto">
|
||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search || ''} onChange={(e) => setSearch(e.target.value || null)} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateSize(true)}>
|
||||
|
||||
@ -8,6 +8,8 @@ import { ProductSize } from '../types/api';
|
||||
import { DeleteProductSizeModal } from './DeleteProductSizeModal';
|
||||
import { EditProductSizeModal } from './EditProductSizeModal';
|
||||
import { DetailProductSizeModal } from './DetailProductSizeModal';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { useDebounce } from '../../../hooks/useDebounce';
|
||||
|
||||
const PAGE_SIZES = [10, 20, 30, 50];
|
||||
|
||||
@ -17,7 +19,10 @@ export const ProductSizesList = () => {
|
||||
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||||
const [selectedSize, setSelectedSize] = useState<ProductSize | null>(null);
|
||||
|
||||
const sizeQuery = useProductSizes();
|
||||
const [searchParam, setSearchParam] = useQueryState('search');
|
||||
const debouncedSearchParam = useDebounce(searchParam, 500);
|
||||
|
||||
const sizeQuery = useProductSizes({ search: debouncedSearchParam });
|
||||
const sizes = sizeQuery.data ?? [];
|
||||
|
||||
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(sizes, {
|
||||
@ -25,10 +30,6 @@ export const ProductSizesList = () => {
|
||||
direction: 'desc',
|
||||
});
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const filteredRecords = paginatedRecords.filter((size) => size.name.toLowerCase().includes(search.toLowerCase()));
|
||||
|
||||
const onDelete = (size: ProductSize) => {
|
||||
setSelectedSize(size);
|
||||
setIsDeleteModalOpen(true);
|
||||
@ -47,12 +48,12 @@ export const ProductSizesList = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
||||
<ProductSizeListHeader search={search} setSearch={setSearch} />
|
||||
<ProductSizeListHeader search={searchParam} setSearch={setSearchParam} />
|
||||
|
||||
<div className="datatables pagination-padding px-5">
|
||||
<DataTable
|
||||
className="whitespace-nowrap table-hover"
|
||||
records={filteredRecords}
|
||||
records={paginatedRecords}
|
||||
columns={productSizeColumns({ onEdit, onView, onDelete })}
|
||||
highlightOnHover
|
||||
minHeight={200}
|
||||
|
||||
@ -22,7 +22,7 @@ export const useCreateProduct = ({ mutationConfig }: UseCreateProductOptions = {
|
||||
mutationFn: createProduct,
|
||||
onSuccess: (...args) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProductsQueryOptions({}).queryKey,
|
||||
queryKey: [getProductsQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(...args);
|
||||
},
|
||||
|
||||
@ -21,7 +21,7 @@ export const useDeleteProduct = ({ mutationConfig }: UseDeleteProductOptions = {
|
||||
mutationFn: deleteProduct,
|
||||
onSuccess: (...args) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProductsQueryOptions({}).queryKey,
|
||||
queryKey: [getProductsQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(...args);
|
||||
},
|
||||
|
||||
@ -5,25 +5,25 @@ import { api } from '../../../lib/apiClient';
|
||||
import { ProductResponse } from '../types/api';
|
||||
|
||||
type ProductsQueryParams = {
|
||||
q?: string;
|
||||
search?: string | null;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export const getProducts = ({ q, page, limit }: ProductsQueryParams): Promise<ProductResponse> => {
|
||||
export const getProducts = ({ search, page, limit }: ProductsQueryParams): Promise<ProductResponse> => {
|
||||
return api.get('/inventory/products', {
|
||||
params: {
|
||||
q,
|
||||
search,
|
||||
page,
|
||||
limit,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getProductsQueryOptions = ({ q, page, limit }: ProductsQueryParams) => {
|
||||
export const getProductsQueryOptions = ({ search, page, limit }: ProductsQueryParams) => {
|
||||
return queryOptions({
|
||||
queryKey: ['products', { q, page, limit }],
|
||||
queryFn: () => getProducts({ q, page, limit }),
|
||||
queryKey: ['products', { search, page, limit }],
|
||||
queryFn: () => getProducts({ search, page, limit }),
|
||||
});
|
||||
};
|
||||
|
||||
@ -31,9 +31,9 @@ type UseProductsOptions = {
|
||||
queryConfig?: QueryConfig<typeof getProductsQueryOptions>;
|
||||
} & ProductsQueryParams;
|
||||
|
||||
export const useProducts = ({ q, page, limit, queryConfig }: UseProductsOptions = {}) => {
|
||||
export const useProducts = ({ search, page, limit, queryConfig }: UseProductsOptions = {}) => {
|
||||
return useQuery({
|
||||
...getProductsQueryOptions({ q, page, limit }),
|
||||
...getProductsQueryOptions({ search, page, limit }),
|
||||
...queryConfig,
|
||||
});
|
||||
};
|
||||
|
||||
@ -21,8 +21,8 @@ export const useUpdateProduct = ({ mutationConfig }: UseUpdateProductOptions = {
|
||||
return useMutation({
|
||||
mutationFn: updateProduct,
|
||||
onSuccess: (data, ...args) => {
|
||||
queryClient.refetchQueries({
|
||||
queryKey: getProductsQueryOptions({}).queryKey,
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [getProductsQueryOptions({}).queryKey[0]],
|
||||
});
|
||||
onSuccess?.(data, ...args);
|
||||
},
|
||||
|
||||
@ -3,14 +3,14 @@ import IconPlus from '../../../components/Icon/IconPlus';
|
||||
import { CreateProductModal } from './CreateProductModal';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const ProductListHeader = ({ search, setSearch }: { search: string; setSearch: (v: string) => void }) => {
|
||||
export const ProductListHeader = ({ search, setSearch }: { search: string | null; setSearch: (v: string | null) => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isOpenModalCreateProduct, setIsOpenModalCreateProduct] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4.5 px-5 flex md:items-center md:flex-row flex-col gap-5">
|
||||
<div className="ltr:mr-auto rtl:ml-auto">
|
||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
<input type="text" className="form-input w-auto" placeholder={t('search')} value={search || ''} onChange={(e) => setSearch(e.target.value || null)} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="btn btn-primary gap-2" onClick={() => setIsOpenModalCreateProduct(true)}>
|
||||
|
||||
@ -8,6 +8,8 @@ import { DeleteProductModal } from './DeleteproductModal';
|
||||
import { Product } from '../types/api';
|
||||
import { EditProductModal } from './EditProductModal';
|
||||
import { DetailProductModal } from './DetailProductModal';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { useDebounce } from '../../../hooks/useDebounce';
|
||||
|
||||
const PAGE_SIZES = [10, 20, 30, 50];
|
||||
|
||||
@ -17,7 +19,10 @@ export const ProductsList = () => {
|
||||
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||||
|
||||
const productsQuery = useProducts();
|
||||
const [searchParam, setSearchParam] = useQueryState('search');
|
||||
const debouncedSearchParam = useDebounce(searchParam, 500);
|
||||
|
||||
const productsQuery = useProducts({ search: debouncedSearchParam });
|
||||
const products = productsQuery.data ?? [];
|
||||
|
||||
const { sortStatus, setSortStatus, page, setPage, pageSize, setPageSize, paginatedRecords, totalRecords } = useSortedPaginatedRecords(products, {
|
||||
@ -25,10 +30,6 @@ export const ProductsList = () => {
|
||||
direction: 'desc',
|
||||
});
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const filteredRecords = paginatedRecords.filter((product) => product.name.toLowerCase().includes(search.toLowerCase()));
|
||||
|
||||
const onDelete = (product: Product) => {
|
||||
setSelectedProduct(product);
|
||||
setIsDeleteModalOpen(true);
|
||||
@ -47,12 +48,12 @@ export const ProductsList = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="panel px-0 border-white-light dark:border-[#1b2e4b] mt-5">
|
||||
<ProductListHeader search={search} setSearch={setSearch} />
|
||||
<ProductListHeader search={searchParam} setSearch={setSearchParam} />
|
||||
|
||||
<div className="datatables pagination-padding px-5">
|
||||
<DataTable
|
||||
className="whitespace-nowrap table-hover"
|
||||
records={filteredRecords}
|
||||
records={paginatedRecords}
|
||||
columns={productColumns({ onEdit, onView, onDelete })}
|
||||
highlightOnHover
|
||||
minHeight={200}
|
||||
|
||||
13
src/hooks/useDebounce.ts
Normal file
13
src/hooks/useDebounce.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useDebounce<T>(value: T, delay = 500): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => setDebouncedValue(value), delay);
|
||||
|
||||
return () => clearTimeout(handler);
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
@ -1,18 +1,23 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { DataTableSortStatus } from 'mantine-datatable';
|
||||
import { sortBy } from 'lodash';
|
||||
import { parseAsInteger, useQueryState } from 'nuqs';
|
||||
|
||||
export function useSortedPaginatedRecords<T>(data: T[] = [], defaultSort: DataTableSortStatus) {
|
||||
// ✅ Sort status pakai useState biasa (tidak sync ke URL)
|
||||
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>(defaultSort);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
// ✅ Pagination pakai URL
|
||||
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
|
||||
const [pageSize, setPageSize] = useQueryState('pageSize', parseAsInteger.withDefault(10));
|
||||
|
||||
const [records, setRecords] = useState<T[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const sorted = sortBy(data, sortStatus.columnAccessor);
|
||||
setRecords(sortStatus.direction === 'desc' ? sorted.reverse() : sorted);
|
||||
setPage(1);
|
||||
}, [data, sortStatus]);
|
||||
setPage(1); // reset ke page 1 kalau sort berubah
|
||||
}, [data, sortStatus, setPage]);
|
||||
|
||||
const paginatedRecords = records.slice((page - 1) * pageSize, page * pageSize);
|
||||
|
||||
|
||||
@ -34,8 +34,13 @@ async function loginFn(dataForm: LoginFormValues): Promise<UsersMeResponse> {
|
||||
}
|
||||
|
||||
async function logoutFn() {
|
||||
const promise = new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
Cookie.clearTokens();
|
||||
return null;
|
||||
resolve(null);
|
||||
}, 1000);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
const authConfig = {
|
||||
|
||||
15
src/main.tsx
15
src/main.tsx
@ -27,20 +27,31 @@ import { queryClient } from './lib/reactQuery';
|
||||
import { AuthLoader } from './lib/auth';
|
||||
import IconLoader from './components/Icon/IconLoader';
|
||||
|
||||
// Nuqs
|
||||
import { NuqsAdapter } from 'nuqs/adapters/react-router/v6';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<Suspense>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex h-screen w-screen items-center justify-center">
|
||||
<IconLoader className="animate-spin w-16 h-16" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryDevtools />
|
||||
<AuthLoader
|
||||
renderLoading={() => (
|
||||
<div className="flex h-screen w-screen items-center justify-center">
|
||||
<IconLoader className="animate-spin" />
|
||||
<IconLoader className="animate-spin w-16 h-16" />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<NuqsAdapter>
|
||||
<RouterProvider router={router} />
|
||||
</NuqsAdapter>
|
||||
</AuthLoader>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user