This commit is contained in:
louiscklaw
2025-04-26 07:14:53 +08:00
parent 6e8fea3bdd
commit 9be92b41d1
5 changed files with 234 additions and 166 deletions

View File

@@ -2,9 +2,9 @@
import * as React from 'react'; import * as React from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import GetAllCount from '@/db/LessonCategories/GetAllCount'; import GetAllCount from '@/db/Vocabularies/GetAllCount';
import GetHiddenCount from '@/db/LessonCategories/GetHiddenCount'; import GetHiddenCount from '@/db/Vocabularies/GetHiddenCount';
import GetVisibleCount from '@/db/LessonCategories/GetVisibleCount'; import GetVisibleCount from '@/db/Vocabularies/GetVisibleCount';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip'; import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';

View File

@@ -5,8 +5,7 @@ import * as React from 'react';
import { useSelection } from '@/hooks/use-selection'; import { useSelection } from '@/hooks/use-selection';
import type { Selection } from '@/hooks/use-selection'; import type { Selection } from '@/hooks/use-selection';
import { Vocabulary } from './type'; import type { Vocabulary } from './type';
function noop(): void { function noop(): void {
return undefined; return undefined;

View File

@@ -5,21 +5,14 @@ import RouterLink from 'next/link';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip'; import Chip from '@mui/material/Chip';
import IconButton from '@mui/material/IconButton';
import LinearProgress from '@mui/material/LinearProgress';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock';
import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images'; import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images';
import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus';
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple'; import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
@@ -29,16 +22,8 @@ import type { ColumnDef } from '@/components/core/data-table';
import ConfirmDeleteModal from './confirm-delete-modal'; import ConfirmDeleteModal from './confirm-delete-modal';
import { useVocabulariesSelection } from './vocabularies-selection-context'; import { useVocabulariesSelection } from './vocabularies-selection-context';
import type { Vocabulary } from './type'; import type { Vocabulary } from './type';
import { listLessonCategories } from '@/db/LessonCategories/listLessonCategories';
import { LessonCategory } from '@/db/LessonCategories/type';
import { Logger } from '@/lib/logger';
import { logger } from '@/lib/default-logger';
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
function columns( function columns(handleDeleteClick: (testId: string) => void): ColumnDef<Vocabulary>[] {
handleDeleteClick: (testId: string) => void,
lessonCategories: { id: string; label: string }[]
): ColumnDef<Vocabulary>[] {
return [ return [
{ {
formatter: (row): React.JSX.Element => ( formatter: (row): React.JSX.Element => (
@@ -173,20 +158,6 @@ export function VocabulariesTable({ rows, reloadRows }: VocabulariesTableProps):
setIdToDelete(testId); setIdToDelete(testId);
} }
let [lessonCategories, setLessonCategories] = React.useState<{}>({});
async function tempFunc(): Promise<void> {
try {
const tempCategories = await listLessonCategories();
console.log(tempCategories);
} catch (error) {
logger.error(error);
}
}
React.useEffect(() => {
void tempFunc;
}, []);
return ( return (
<React.Fragment> <React.Fragment>
<ConfirmDeleteModal <ConfirmDeleteModal

View File

@@ -2,75 +2,85 @@
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { COL_VOCABULARIES } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { Avatar, Divider } from '@mui/material'; import { Avatar, Divider, MenuItem } from '@mui/material';
// import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions'; import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent'; import CardContent from '@mui/material/CardContent';
import FormControl from '@mui/material/FormControl'; import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText'; import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel'; import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput'; import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2'; import Grid from '@mui/material/Unstable_Grid2';
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
// import axios from 'axios';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod'; import { z as zod } from 'zod';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { fileToBase64 } from '@/lib/file-to-base64'; import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
import { Option } from '@/components/core/option';
import { TextEditor } from '@/components/core/text-editor/text-editor'; import { TextEditor } from '@/components/core/text-editor/text-editor';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading';
import ErrorDisplay from '../error';
import type { EditFormProps } from './type';
import { listLessonCategories } from '@/db/LessonCategories/listLessonCategories';
import isDevelopment from '@/lib/check-is-development';
const schema = zod.object({ const schema = zod.object({
image: zod.union([zod.array(zod.any()), zod.string()]).optional(),
sound: zod.union([zod.array(zod.any()), zod.string()]).optional(),
word: zod.string().min(1, 'Word is required').max(255),
word_c: zod.string().min(1, 'Chinese word is required').max(255),
sample_e: zod.string().optional(),
sample_c: zod.string().optional(),
cat_id: zod.string().min(1, 'Category ID is required'),
category: zod.string().optional(),
lesson_type_id: zod.string().optional(),
// NOTE: for image handling
avatar: zod.string().optional(), avatar: zod.string().optional(),
name: zod.string().min(1, 'Name is required').max(255), visible: zod.string(),
email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255),
phone: zod.string().min(1, 'Phone is required').max(15),
company: zod.string().max(255),
billingAddress: zod.object({
country: zod.string().min(1, 'Country is required').max(255),
state: zod.string().min(1, 'State is required').max(255),
city: zod.string().min(1, 'City is required').max(255),
zipCode: zod.string().min(1, 'Zip code is required').max(255),
line1: zod.string().min(1, 'Street line 1 is required').max(255),
line2: zod.string().max(255).optional(),
}),
taxId: zod.string().max(255).optional(),
timezone: zod.string().min(1, 'Timezone is required').max(255),
language: zod.string().min(1, 'Language is required').max(255),
currency: zod.string().min(1, 'Currency is required').max(255),
}); });
type Values = zod.infer<typeof schema>; type Values = zod.infer<typeof schema>;
const defaultValues = { const defaultValues = {
avatar: '', image: undefined,
name: '', sound: undefined,
email: '', word: '',
phone: '', word_c: '',
company: '', sample_e: '',
billingAddress: { country: '', state: '', city: '', zipCode: '', line1: '', line2: '' }, sample_c: '',
taxId: '', cat_id: '',
timezone: 'new_york', category: '',
language: 'en', lesson_type_id: '',
currency: 'USD', visible: 'visible',
} satisfies Values; } satisfies Values;
export function VocabularyCreateForm(): React.JSX.Element { export function VocabularyCreateForm(): React.JSX.Element {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(['common', 'lesson_category']); const { t } = useTranslation();
const { cat_id: catId } = useParams<{ cat_id: string }>();
const [isCreating, setIsCreating] = React.useState<boolean>(false); const [isCreating, setIsCreating] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(false);
//
const [showError, setShowError] = React.useState({ show: false, detail: '' });
const { const {
control, control,
@@ -83,15 +93,33 @@ export function VocabularyCreateForm(): React.JSX.Element {
const onSubmit = React.useCallback( const onSubmit = React.useCallback(
async (values: Values): Promise<void> => { async (values: Values): Promise<void> => {
setIsCreating(true); setIsCreating(true);
const tempUpdate: EditFormProps = {
image: values.avatar ? [await base64ToFile(values.avatar)] : null,
sound: '',
word: values.word,
word_c: values.word_c,
sample_e: values.sample_e || '',
sample_c: values.sample_c || '',
cat_id: values.cat_id,
category: '',
lesson_type_id: '',
};
try { try {
// Make API request const result = await pb.collection(COL_VOCABULARIES).create(tempUpdate);
toast.success('Customer updated'); logger.debug(result);
router.push(paths.dashboard.lesson_categories.details('1')); toast.success(t('create.success'));
// router.push(paths.dashboard.lesson_categories.details('1'));
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
toast.error('Something went wrong!'); toast.error(t('create.failed'));
} finally {
setIsCreating(false);
} }
}, },
// t is not necessary here
// eslint-disable-next-line react-hooks/exhaustive-deps
[router] [router]
); );
@@ -110,6 +138,43 @@ export function VocabularyCreateForm(): React.JSX.Element {
[setValue] [setValue]
); );
const [categoriesOption, setCategoriesOption] = React.useState<{ value: string; label: string }[]>([]);
const loadCategories = React.useCallback(async () => {
try {
const categories = await listLessonCategories();
logger.debug(categories);
setCategoriesOption(
categories.map((c) => {
return { label: c.cat_name || '??', value: c.id };
})
);
} catch (error) {
logger.error(error);
toast.error(t('list.error'));
}
}, [catId]);
React.useEffect(() => {
setShowLoading(true);
void loadCategories();
//
setShowLoading(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [catId]);
if (showLoading) return <FormLoading />;
if (showError.show)
return (
<ErrorDisplay
message={t('error.unable-to-process-request')}
code="500"
details={showError.detail}
/>
);
return ( return (
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<Card> <Card>
@@ -183,84 +248,125 @@ export function VocabularyCreateForm(): React.JSX.Element {
xs={12} xs={12}
> >
<Controller <Controller
disabled={isCreating}
control={control} control={control}
name="name" name="word"
render={({ field }) => ( render={({ field }) => (
<FormControl <FormControl
error={Boolean(errors.name)} disabled={isCreating}
error={Boolean(errors.word)}
fullWidth fullWidth
> >
<InputLabel required>{t('create.name')}</InputLabel> <InputLabel required>{t('create.word')}</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput {...field} />
{errors.name ? <FormHelperText>{errors.name.message}</FormHelperText> : null} {errors.word ? <FormHelperText>{errors.word.message}</FormHelperText> : null}
</FormControl> </FormControl>
)} )}
/> />
</Grid> </Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="word_c"
render={({ field }) => (
<FormControl
error={Boolean(errors.word_c)}
fullWidth
>
<InputLabel required>{t('create.word_c')}</InputLabel>
<OutlinedInput {...field} />
{errors.word_c ? <FormHelperText>{errors.word_c.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="cat_id"
render={({ field }) => (
<FormControl
error={Boolean(errors.cat_id)}
fullWidth
>
<InputLabel>{t('create.cat_id')}</InputLabel>
<Select {...field}>
{categoriesOption.map((co, i) => (
<Option
key={i}
value={co.value}
>
{co.label}
</Option>
))}
</Select>
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="sound"
render={({ field }) => (
<FormControl
error={Boolean(errors.sound)}
fullWidth
>
{/* TODO: sound file selection is not implemented */}
<InputLabel required>{t('create.sound_file')} - (Not implemented)</InputLabel>
<OutlinedInput {...field} />
{errors.sound ? <FormHelperText>{errors.sound.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid <Grid
md={6} md={6}
xs={12} xs={12}
> >
<Controller <Controller
control={control} control={control}
name="email" name="visible"
render={({ field }) => ( render={({ field }) => (
<FormControl <FormControl
error={Boolean(errors.email)} error={Boolean(errors.visible)}
fullWidth fullWidth
> >
<InputLabel required>Email address</InputLabel> <InputLabel>{t('create.visible')}</InputLabel>
<OutlinedInput <Select {...field}>
{...field} <MenuItem value="visible">visible</MenuItem>
type="email" <MenuItem value="hidden">hidden</MenuItem>
/> </Select>
{errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null}
</FormControl> {errors.visible ? <FormHelperText>{errors.visible.message}</FormHelperText> : null}
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="phone"
render={({ field }) => (
<FormControl
error={Boolean(errors.phone)}
fullWidth
>
<InputLabel required>Phone number</InputLabel>
<OutlinedInput {...field} />
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="company"
render={({ field }) => (
<FormControl
error={Boolean(errors.company)}
fullWidth
>
<InputLabel>Company</InputLabel>
<OutlinedInput {...field} />
{errors.company ? <FormHelperText>{errors.company.message}</FormHelperText> : null}
</FormControl> </FormControl>
)} )}
/> />
</Grid> </Grid>
</Grid> </Grid>
</Stack> </Stack>
{/* */}
<Stack spacing={3}> <Stack spacing={3}>
<Typography variant="h6">{t('create.detail-information')}</Typography> <Typography variant="h6">{t('create.sample-sentence')}</Typography>
<Grid <Grid
container container
spacing={3} spacing={3}
@@ -270,23 +376,22 @@ export function VocabularyCreateForm(): React.JSX.Element {
xs={12} xs={12}
> >
<Controller <Controller
disabled={isCreating}
control={control} control={control}
name="billingAddress.country" name="sample_e"
render={({ field }) => ( render={({ field }) => (
<Box> <FormControl
<Typography error={Boolean(errors.sample_e)}
variant="subtitle1" fullWidth
color="text-secondary" >
> <InputLabel>{t('create.sample_e')}</InputLabel>
{t('create.description')} <OutlinedInput
</Typography> {...field}
<Box sx={{ mt: '8px', '& .tiptap-container': { height: '400px' } }}> multiline
<TextEditor rows={4}
content="" />
placeholder="Write something" {errors.sample_e ? <FormHelperText>{errors.sample_e.message}</FormHelperText> : null}
/> </FormControl>
</Box>
</Box>
)} )}
/> />
</Grid> </Grid>
@@ -295,35 +400,35 @@ export function VocabularyCreateForm(): React.JSX.Element {
xs={12} xs={12}
> >
<Controller <Controller
disabled={isCreating}
control={control} control={control}
name="billingAddress.state" name="sample_c"
render={({ field }) => ( render={({ field }) => (
<Box> <FormControl
<Typography error={Boolean(errors.sample_c)}
variant="subtitle1" fullWidth
color="text.secondary" >
> <InputLabel>{t('create.sample_c')}</InputLabel>
{t('create.remarks')} <OutlinedInput
</Typography> {...field}
<Box sx={{ mt: '8px', '& .tiptap-container': { height: '400px' } }}> multiline
<TextEditor rows={4}
content="" />
placeholder="Write something" {errors.sample_c ? <FormHelperText>{errors.sample_c.message}</FormHelperText> : null}
/> </FormControl>
</Box>
</Box>
)} )}
/> />
</Grid> </Grid>
</Grid> </Grid>
</Stack> </Stack>
{/* */}
</Stack> </Stack>
</CardContent> </CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}> <CardActions sx={{ justifyContent: 'flex-end' }}>
<Button <Button
color="secondary" color="secondary"
component={RouterLink} component={RouterLink}
href={paths.dashboard.lesson_categories.list} href={paths.dashboard.vocabularies.list}
> >
{t('create.cancelButton')} {t('create.cancelButton')}
</Button> </Button>
@@ -337,6 +442,9 @@ export function VocabularyCreateForm(): React.JSX.Element {
{t('create.createButton')} {t('create.createButton')}
</LoadingButton> </LoadingButton>
</CardActions> </CardActions>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify(errors, null, 2)}</pre>
</Box>
</Card> </Card>
</form> </form>
); );

View File

@@ -6,8 +6,7 @@ import { useParams, useRouter } from 'next/navigation';
import { COL_VOCABULARIES } from '@/constants'; import { COL_VOCABULARIES } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { Avatar, Divider, MenuItem } from '@mui/material'; import { Avatar, Divider } from '@mui/material';
// import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
@@ -23,26 +22,20 @@ import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2'; import Grid from '@mui/material/Unstable_Grid2';
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
import type { RecordModel } from 'pocketbase';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod'; import { z as zod } from 'zod';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { dayjs } from '@/lib/dayjs';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb'; import { pb } from '@/lib/pb';
import { Option } from '@/components/core/option'; import { Option } from '@/components/core/option';
import { TextEditor } from '@/components/core/text-editor/text-editor';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading'; import FormLoading from '@/components/loading';
import ErrorDisplay from '../error'; import ErrorDisplay from '../error';
import { defaultLessonCategory } from './_constants';
import type { EditFormProps } from './type'; import type { EditFormProps } from './type';
import getVocabularyById from '@/db/Vocabularies/GetById';
import getAllLessonCategories from '@/db/LessonCategories/GetAll';
import { listLessonCategories } from '@/db/LessonCategories/listLessonCategories'; import { listLessonCategories } from '@/db/LessonCategories/listLessonCategories';
import isDevelopment from '@/lib/check-is-development'; import isDevelopment from '@/lib/check-is-development';
@@ -55,7 +48,7 @@ const schema = zod.object({
sample_c: zod.string().optional(), sample_c: zod.string().optional(),
cat_id: zod.string().min(1, 'Category ID is required'), cat_id: zod.string().min(1, 'Category ID is required'),
category: zod.string().optional(), category: zod.string().optional(),
lesson_type_id: zod.string().min(1, 'Lesson type ID is required'), lesson_type_id: zod.string().optional(),
// NOTE: for image handling // NOTE: for image handling
avatar: zod.string().optional(), avatar: zod.string().optional(),
}); });
@@ -111,7 +104,6 @@ export function VocabularyEditForm(): React.JSX.Element {
}; };
// //
try { try {
console.log({ tempUpdate });
const result = await pb.collection(COL_VOCABULARIES).update(catId, tempUpdate); const result = await pb.collection(COL_VOCABULARIES).update(catId, tempUpdate);
logger.debug(result); logger.debug(result);
toast.success(t('edit.success')); toast.success(t('edit.success'));
@@ -173,7 +165,7 @@ export function VocabularyEditForm(): React.JSX.Element {
[catId] [catId]
); );
let [categoriesOption, setCategoriesOption] = React.useState<{ value: string; label: string }[]>([]); const [categoriesOption, setCategoriesOption] = React.useState<{ value: string; label: string }[]>([]);
const loadCategories = React.useCallback(async () => { const loadCategories = React.useCallback(async () => {
try { try {
const categories = await listLessonCategories(); const categories = await listLessonCategories();
@@ -193,10 +185,8 @@ export function VocabularyEditForm(): React.JSX.Element {
React.useEffect(() => { React.useEffect(() => {
setShowLoading(true); setShowLoading(true);
void loadCategories(); void loadCategories();
void loadExistingData(catId); void loadExistingData(catId);
setShowLoading(false); setShowLoading(false);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps