Compare commits

..

9 Commits

Author SHA1 Message Date
louiscklaw
73a2b2dfb9 update, 2025-04-22 13:30:02 +08:00
louiscklaw
2f28d71060 update page.tsx visual=true filter, 2025-04-22 13:19:12 +08:00
louiscklaw
01ce517629 update fix import path, 2025-04-22 12:29:56 +08:00
louiscklaw
033ca95dfe refactor to dedicated directory, 2025-04-22 12:20:21 +08:00
louiscklaw
0fa277c50e update schema and seed, 2025-04-22 12:13:42 +08:00
louiscklaw
e8ded0cb30 update for filter, 2025-04-22 11:57:24 +08:00
louiscklaw
cc6e40c9d0 update delete lp_categories and lp_questions, 2025-04-22 11:55:18 +08:00
louiscklaw
407d851b92 update mf build ok, 2025-04-22 04:40:01 +08:00
louiscklaw
0b38de74a2 update refactoring, 2025-04-22 04:18:53 +08:00
112 changed files with 4406 additions and 6451 deletions

View File

@@ -0,0 +1,15 @@
# guideline
- please divide the problem into small parts
- if you found youself cannot understand the problem, please stop and ask how to do
- if you found youself cannot solve the problem, plesae stop and ask how to do
- review the whole solution before you reply to user
- if code syntax is already there, do follow (e.g. naming convention, syntax) the existing code
- example for page can be found in `./src/app/_helloworld/page.tsx`
- example for component can be found in `./src/components/_helloworld/index.tsx`
- no need to explain the reason until you are told to do so
- no need to show me the code change, at the end just simple summary in point form is ok
Thanks

View File

@@ -0,0 +1,99 @@
# AI GUIDELINE
## getting started
Imagine there is a software developer and a QA engineer to solve the problems together
They will:
no need to reply me what you are going on and your digest in this phase.
just reply me "OK" when done
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
- `schema.dbml`
- read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
this is file in dbml syntax state the main database
- `schema.json`
- read `<base_dir>/002_source/cms/src/db/schema.json`
this is the file of live pocketbase schema output
- read `<base_dir>/002_source/cms/src/constants.ts`
this is the content of `@/constants`
- look into the md files in folder `<base_dir>/002_source/cms/_AI_WORKSPACE/001_guideline`
- read, remember and link up the ideas in file stated above,
i will tell them the task afterwards
---
The software engineer will provide solutions,
while QA engineer will feedback the opinion.
this is now not in debug phase,
so, no need to reply me what they are going on or their insight throught the prompt.
just reply me "OK" when done
---
clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it
please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden`
well done !, please proceed to another request
working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db`
according information from `schema.json`, get the collection of `Students`
pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content
when you draft coding, review file and append with `.tsx.draft`
---
- this is part of react typescript project, with pocketbase
- `schema.dbml`, describe the collections(tables)
- folder `LessonCategories`, the correct references
- folder `LessonTypes`, the correct references
- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006`
- do not read root directory, assume it is a fresh copy of nextjs project is ok
## instruction
- break the questions into smaller parts
- review file append with `.draft`, see if the content aligned with the correct references
- read and understand `dbml` file
- lookup the every folder
## tasks
Thanks
---
---
please revise
please revise
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory`
to the collection `QuizLPCategories` align the dbml file in the previous prompt
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx`
to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory`
---
the constants file (`@/constants`) was `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts`
please help to fix the `tsx` files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizMFCategories`,
the `COL` constants is wrongly used, it should refer to `COL_QUIZ_MF_CATEGORIES`. thanks
please update the `COL_XXXX` TO COL_MF_CATEGORIES

View File

@@ -0,0 +1,10 @@
Hi, i need your help.
i am working on a react typescript project
i will show you part of the code and it is located in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/mf`
it is copied from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lp`, i want you to help modifing from `lp` to `mf`, the corrosponding word is `lp -> listening practice` and `mf -> matching frenzy`,
please help to update the `imports`, variables/constants, functions, classes
thank you

View File

@@ -12,9 +12,9 @@ import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
import { useTranslation } from 'react-i18next';
import type { Address } from '@/types/Address';
import { ShippingAddress } from '@/components/dashboard/lp_categories/shipping-address';
import { ShippingAddress } from '@/components/dashboard/lp/categories/shipping-address';
import { SampleAddresses } from '../SampleAddresses';
import { SampleAddresses } from './SampleAddresses';
export default function SampleAddressCard(): React.JSX.Element {
const { t } = useTranslation();
@@ -22,7 +22,10 @@ export default function SampleAddressCard(): React.JSX.Element {
<Card>
<CardHeader
action={
<Button color="secondary" startIcon={<PlusIcon />}>
<Button
color="secondary"
startIcon={<PlusIcon />}
>
{t('list.add')}
</Button>
}
@@ -34,9 +37,16 @@ export default function SampleAddressCard(): React.JSX.Element {
title={t('list.shipping-addresses')}
/>
<CardContent>
<Grid container spacing={3}>
<Grid
container
spacing={3}
>
{(SampleAddresses satisfies Address[]).map((address) => (
<Grid key={address.id} md={6} xs={12}>
<Grid
key={address.id}
md={6}
xs={12}
>
<ShippingAddress address={address} />
</Grid>
))}

View File

@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list';
import { Payments } from '@/components/dashboard/lp_categories/payments';
import { Payments } from '@/components/dashboard/lp/categories/payments';
import { SamplePayments } from './SamplePayments';
@@ -21,11 +21,19 @@ export default function SamplePaymentCard(): React.JSX.Element {
const { t } = useTranslation();
return (
<>
<Payments ordersValue={2069.48} payments={SamplePayments} refundsValue={324.5} totalOrders={5} />
<Payments
ordersValue={2069.48}
payments={SamplePayments}
refundsValue={324.5}
totalOrders={5}
/>
<Card>
<CardHeader
action={
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
<Button
color="secondary"
startIcon={<PencilSimpleIcon />}
>
{t('list.edit')}
</Button>
}
@@ -37,8 +45,14 @@ export default function SamplePaymentCard(): React.JSX.Element {
title={t('list.billing-details')}
/>
<CardContent>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}>
<Card
sx={{ borderRadius: 1 }}
variant="outlined"
>
<PropertyList
divider={<Divider />}
sx={{ '--PropertyItem-padding': '16px' }}
>
{(
[
{ key: t('Credit card'), value: '**** 4142' },
@@ -50,7 +64,11 @@ export default function SamplePaymentCard(): React.JSX.Element {
] satisfies { key: string; value: React.ReactNode }[]
).map(
(item): React.JSX.Element => (
<PropertyItem key={item.key} name={item.key} value={item.value} />
<PropertyItem
key={item.key}
name={item.key}
value={item.value}
/>
)
)}
</PropertyList>

View File

@@ -27,11 +27,17 @@ export default function SampleSecurityCard(): React.JSX.Element {
<CardContent>
<Stack spacing={1}>
<div>
<Button color="error" variant="contained">
<Button
color="error"
variant="contained"
>
{t('Delete account')}
</Button>
</div>
<Typography color="text.secondary" variant="body2">
<Typography
color="text.secondary"
variant="body2"
>
{t('a-deleted-customer-cannot-be-restored-all-data-will-be-permanently-removed')}
</Typography>
</Stack>

View File

@@ -1,5 +1,9 @@
'use client';
// RULES:
// contains list page for lesson_categories (LessonCategories)
// contain definition to collection only
//
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { COL_LESSON_CATEGORIES } from '@/constants';
@@ -14,6 +18,7 @@ import type { ListResult, RecordModel } from 'pocketbase';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster';
@@ -24,7 +29,7 @@ import type { Filters } from '@/components/dashboard/lesson_category/lesson-cate
import { LessonCategoriesPagination } from '@/components/dashboard/lesson_category/lesson-categories-pagination';
import { LessonCategoriesSelectionProvider } from '@/components/dashboard/lesson_category/lesson-categories-selection-context';
import { LessonCategoriesTable } from '@/components/dashboard/lesson_category/lesson-categories-table';
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
import FormLoading from '@/components/loading';
@@ -39,60 +44,138 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState<boolean>(false);
const [showError, setShowError] = React.useState({ show: false, detail: '' });
//
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
//
const [f, setF] = React.useState<LessonCategory[]>([]);
const [currentPage, setCurrentPage] = React.useState<number>(1);
//
const [recordCount, setRecordCount] = React.useState<number>(0);
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status: status });
const [listOption, setListOption] = React.useState({});
const [listSort, setListSort] = React.useState({});
//
const reloadRows = () => {
setShowLoading(true);
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status });
pb.collection(COL_LESSON_CATEGORIES)
.getList(currentPage, rowsPerPage, {})
.then((lessonCategories: ListResult<RecordModel>) => {
// console.log(lessonTypes);
const { items, page, perPage, totalItems, totalPages } = lessonCategories;
const tempLessonCategories: LessonCategory[] = items.map((item) => {
return { ...defaultLessonCategory, ...item };
});
setLessonCategoriesData(tempLessonCategories);
setRecordCount(totalItems);
})
.catch((err) => {
logger.error(err);
toast(t('dashboard.lessonTypes.list.error'));
setShowError(true);
})
.finally(() => {
setShowLoading(false);
//
const reloadRows = async (): Promise<void> => {
try {
const models: ListResult<RecordModel> = await pb
.collection(COL_LESSON_CATEGORIES)
.getList(currentPage + 1, rowsPerPage, listOption);
const { items, totalItems } = models;
const tempLessonTypes: LessonCategory[] = items.map((lt) => {
return { ...defaultLessonCategory, ...lt };
});
setLessonCategoriesData(tempLessonTypes);
setRecordCount(totalItems);
setF(tempLessonTypes);
// console.log({ currentPage, f });
} catch (error) {
//
logger.error(error);
setShowError({
//
show: true,
detail: JSON.stringify(error, null, 2),
});
} finally {
setShowLoading(false);
}
// pb.collection(COL_LESSON_CATEGORIES)
// .getList(currentPage, rowsPerPage, listOption)
// .then((lessonCategories: ListResult<RecordModel>) => {
// // console.log(lessonTypes);
// const { items, page, perPage, totalItems, totalPages } = lessonCategories;
// const tempLessonCategories: LessonCategory[] = items.map((item) => {
// return { ...defaultLessonCategory, ...item };
// });
// setLessonCategoriesData(tempLessonCategories);
// setRecordCount(totalItems);
// setF(tempLessonCategories);
// // console.log({ currentPage, f });
// })
// .catch((error) => {
// logger.error(error);
// setShowError({
// //
// show: true,
// detail: JSON.stringify(error, null, 2),
// });
// })
// .finally(() => {
// setShowLoading(false);
// });
};
const [lastListOption, setLastListOption] = React.useState({});
const isFirstRun = React.useRef(false);
React.useEffect(() => {
reloadRows();
}, []);
if (!isFirstRun.current) {
isFirstRun.current = true;
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
// reset page number as tab changes
setLastListOption(listOption);
setCurrentPage(0);
void reloadRows();
} else {
void reloadRows();
}
}, [currentPage, rowsPerPage, listOption]);
React.useEffect(() => {
let tempFilter = [],
tempSortDir = '';
if (visible) {
tempFilter.push(`visible = "${visible}"`);
}
if (sortDir) {
tempSortDir = `-created`;
}
if (name) {
tempFilter.push(`name ~ "%${name}%"`);
}
if (type) {
tempFilter.push(`type ~ "%${type}%"`);
}
let preFinalListOption = {};
if (tempFilter.length > 0) {
preFinalListOption = { filter: tempFilter.join(' && ') };
}
if (tempSortDir.length > 0) {
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
}
setListOption(preFinalListOption);
// setListOption({
// filter: tempFilter.join(' && '),
// sort: tempSortDir,
// //
// });
}, [visible, sortDir, name, type]);
// return <>helloworld</>;
if (showLoading) return <FormLoading />;
if (showError)
if (showError.show)
return (
<ErrorDisplay
message={t('error.unable-to-process-request', { ns: 'common' })}
message={t('error.unable-to-process-request')}
code="500"
details={t('error.detailed-error-information', { ns: 'common' })}
details={showError.detail}
/>
);
// return <pre>{JSON.stringify(lessonCategoriesData, null, 2)}</pre>;
return (
<Box
sx={{
@@ -103,7 +186,11 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
}}
>
<Stack spacing={4}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={3}
sx={{ alignItems: 'flex-start' }}
>
<Box sx={{ flex: '1 1 auto' }}>
<Typography variant="h4">{t('list.title')}</Typography>
</Box>
@@ -117,6 +204,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
startIcon={<PlusIcon />}
variant="contained"
>
{/* add new lesson type */}
{t('list.add')}
</LoadingButton>
</Box>
@@ -130,13 +218,25 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
/>
<Divider />
<Box sx={{ overflowX: 'auto' }}>
<LessonCategoriesTable reloadRows={reloadRows} rows={filteredLessonCategories} />
<LessonCategoriesTable
reloadRows={reloadRows}
rows={f}
/>
</Box>
<Divider />
<LessonCategoriesPagination count={filteredLessonCategories.length + 100} page={0} />
<LessonCategoriesPagination
count={recordCount}
page={currentPage}
rowsPerPage={rowsPerPage}
setPage={setCurrentPage}
setRowsPerPage={setRowsPerPage}
/>
</Card>
</LessonCategoriesSelectionProvider>
</Stack>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify(f, null, 2)}</pre>
</Box>
</Box>
);
}
@@ -153,19 +253,19 @@ function applySort(row: LessonCategory[], sortDir: 'asc' | 'desc' | undefined):
});
}
function applyFilters(row: LessonCategory[], { email, phone, status }: Filters): LessonCategory[] {
function applyFilters(row: LessonCategory[], { email, phone, status, name, visible }: Filters): LessonCategory[] {
return row.filter((item) => {
// if (email) {
// if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
// return false;
// }
// }
if (email) {
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
return false;
}
}
// if (phone) {
// if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
// return false;
// }
// }
if (phone) {
if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
return false;
}
}
if (status) {
if (item.status !== status) {
@@ -173,6 +273,18 @@ function applyFilters(row: LessonCategory[], { email, phone, status }: Filters):
}
}
if (name) {
if (!item.name?.toLowerCase().includes(name.toLowerCase())) {
return false;
}
}
if (visible) {
if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) {
return false;
}
}
return true;
});
}
@@ -186,6 +298,5 @@ interface PageProps {
name?: string;
visible?: string;
type?: string;
//
};
}

View File

@@ -6,9 +6,9 @@ import { useParams, useRouter } from 'next/navigation';
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
import BasicDetailCard from '@/app/dashboard/Sample/BasicDetailCard';
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
import SamplePaymentCard from '@/app/dashboard/Sample/SamplePaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
import SampleTitleCard from '@/app/dashboard/Sample/SampleTitleCard';
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
import SampleTitleCard from '@/app/dashboard/Sample/TitleCard';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';

View File

@@ -1,7 +1,7 @@
'use client';
// RULES:
// contains list page for lp_categories (QuizLPCategories)
// contains list page for lesson_types (LessonTypes)
// contain definition to collection only
//
import * as React from 'react';
@@ -18,6 +18,7 @@ import type { ListResult, RecordModel } from 'pocketbase';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster';
@@ -43,8 +44,10 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const [showError, setShowError] = React.useState({ show: false, detail: '' });
//
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
//
const [f, setF] = React.useState<LessonType[]>([]);
const [currentPage, setCurrentPage] = React.useState<number>(0);
//
const [recordCount, setRecordCount] = React.useState<number>(0);
const [listOption, setListOption] = React.useState({});
const [listSort, setListSort] = React.useState({});
@@ -63,17 +66,33 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
setLessonTypesData(tempLessonTypes);
setRecordCount(totalItems);
setF(tempLessonTypes);
// console.log({ currentPage, f });
} catch (error) {
//
logger.error(error);
setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
setShowError({
//
show: true,
detail: JSON.stringify(error, null, 2),
});
} finally {
setShowLoading(false);
}
};
const [lastListOption, setLastListOption] = React.useState({});
const isFirstRun = React.useRef(false);
React.useEffect(() => {
void reloadRows();
if (!isFirstRun.current) {
isFirstRun.current = true;
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
// reset page number as tab changes
setLastListOption(listOption);
setCurrentPage(0);
void reloadRows();
} else {
void reloadRows();
}
}, [currentPage, rowsPerPage, listOption]);
React.useEffect(() => {
@@ -96,16 +115,26 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
tempFilter.push(`type ~ "%${type}%"`);
}
setListOption({
filter: tempFilter.join(' && '),
sort: tempSortDir,
//
});
let preFinalListOption = {};
if (tempFilter.length > 0) {
preFinalListOption = { filter: tempFilter.join(' && ') };
}
if (tempSortDir.length > 0) {
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
}
setListOption(preFinalListOption);
// setListOption({
// filter: tempFilter.join(' && '),
// sort: tempSortDir,
// //
// });
}, [visible, sortDir, name, type]);
if (f.length === 0 || showLoading) return <FormLoading />;
// return <>helloworld</>;
if (showError)
if (showLoading) return <FormLoading />;
if (showError.show)
return (
<ErrorDisplay
message={t('error.unable-to-process-request')}
@@ -143,6 +172,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
variant="contained"
>
{/* add new lesson type */}
{/* TODO: refactor translations.json */}
{t('dashboard.lessonTypes.add')}
</LoadingButton>
</Box>
@@ -153,14 +183,12 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
filters={{ email, phone, status, name, visible, type }}
fullData={lessonTypesData}
sortDir={sortDir}
//
/>
<Divider />
<Box sx={{ overflowX: 'auto' }}>
<LessonTypesTable
reloadRows={reloadRows}
rows={f}
//
/>
</Box>
<Divider />
@@ -174,6 +202,9 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
</Card>
</LessonTypesSelectionProvider>
</Stack>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify(f, null, 2)}</pre>
</Box>
</Box>
);
}
@@ -235,6 +266,5 @@ interface PageProps {
name?: string;
visible?: string;
type?: string;
//
};
}

View File

@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list';
import { LpCategory } from '@/components/dashboard/lp_categories/type';
import { LpCategory } from '@/components/dashboard/lp/categories/type';
export default function BasicDetailCard({
lpModel: model,

View File

@@ -10,7 +10,7 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
import { useTranslation } from 'react-i18next';
import { LpCategory } from '@/components/dashboard/lp_categories/type';
import { LpCategory } from '@/components/dashboard/lp/categories/type';
function getImageUrlFrRecord(record: LpCategory): string {
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;

View File

@@ -5,8 +5,8 @@ import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
import SamplePaymentCard from '@/app/dashboard/Sample/SamplePaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
@@ -21,9 +21,9 @@ import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultLpCategory } from '@/components/dashboard/lp_categories/_constants.ts';
import { Notifications } from '@/components/dashboard/lp_categories/notifications';
import type { LpCategory } from '@/components/dashboard/lp_categories/type';
import { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants.ts';
import { Notifications } from '@/components/dashboard/lp/categories/notifications';
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
import FormLoading from '@/components/loading';
import BasicDetailCard from './BasicDetailCard';

View File

@@ -13,7 +13,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { LpCategoryCreateForm } from '@/components/dashboard/lp_categories/lp-category-create-form';
import { LpCategoryCreateForm } from '@/components/dashboard/lp/categories/lp-category-create-form';
export default function Page(): React.JSX.Element {
// RULES: follow the name of page directory

View File

@@ -10,7 +10,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { LpCategoryEditForm } from '@/components/dashboard/lp_categories/lp-category-edit-form';
import { LpCategoryEditForm } from '@/components/dashboard/lp/categories/lp-category-edit-form';
export default function Page(): React.JSX.Element {
const { t } = useTranslation(['lp_categories']);

View File

@@ -18,17 +18,18 @@ import type { ListResult, RecordModel } from 'pocketbase';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultLpCategory } from '@/components/dashboard/lp_categories/_constants';
import { LpCategoriesFilters } from '@/components/dashboard/lp_categories/lp-categories-filters';
import type { Filters } from '@/components/dashboard/lp_categories/lp-categories-filters';
import { LpCategoriesPagination } from '@/components/dashboard/lp_categories/lp-categories-pagination';
import { LpCategoriesSelectionProvider } from '@/components/dashboard/lp_categories/lp-categories-selection-context';
import { LpCategoriesTable } from '@/components/dashboard/lp_categories/lp-categories-table';
import type { LpCategory } from '@/components/dashboard/lp_categories/type';
import { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants';
import { LpCategoriesFilters } from '@/components/dashboard/lp/categories/lp-categories-filters';
import type { Filters } from '@/components/dashboard/lp/categories/lp-categories-filters';
import { LpCategoriesPagination } from '@/components/dashboard/lp/categories/lp-categories-pagination';
import { LpCategoriesSelectionProvider } from '@/components/dashboard/lp/categories/lp-categories-selection-context';
import { LpCategoriesTable } from '@/components/dashboard/lp/categories/lp-categories-table';
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
import FormLoading from '@/components/loading';
export default function Page({ searchParams }: PageProps): React.JSX.Element {
@@ -43,8 +44,10 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const [showError, setShowError] = React.useState({ show: false, detail: '' });
//
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
//
const [f, setF] = React.useState<LpCategory[]>([]);
const [currentPage, setCurrentPage] = React.useState<number>(1);
const [currentPage, setCurrentPage] = React.useState<number>(0);
//
const [recordCount, setRecordCount] = React.useState<number>(0);
const [listOption, setListOption] = React.useState({});
const [listSort, setListSort] = React.useState({});
@@ -53,11 +56,12 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status });
//
const reloadRows = async (): Promise<void> => {
try {
const models: ListResult<RecordModel> = await pb
.collection(COL_QUIZ_LP_CATEGORIES)
.getList(currentPage + 1, rowsPerPage, {});
.getList(currentPage + 1, rowsPerPage, listOption);
const { items, totalItems } = models;
const tempLessonTypes: LpCategory[] = items.map((lt) => {
return { ...defaultLpCategory, ...lt };
@@ -66,26 +70,81 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
setLessonCategoriesData(tempLessonTypes);
setRecordCount(totalItems);
setF(tempLessonTypes);
console.log({ currentPage, f });
// console.log({ currentPage, f });
} catch (error) {
//
setShowError({ show: true, detail: JSON.stringify(error) });
logger.error(error);
setShowError({
//
show: true,
detail: JSON.stringify(error, null, 2),
});
} finally {
setShowLoading(false);
}
};
const [lastListOption, setLastListOption] = React.useState({});
const isFirstRun = React.useRef(false);
React.useEffect(() => {
void reloadRows();
if (!isFirstRun.current) {
isFirstRun.current = true;
} else {
if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
// reset page number as tab changes
setLastListOption(listOption);
setCurrentPage(0);
void reloadRows();
} else {
void reloadRows();
}
}
}, [currentPage, rowsPerPage, listOption]);
React.useEffect(() => {
let tempFilter = [],
tempSortDir = '';
if (visible) {
tempFilter.push(`visible = "${visible}"`);
}
if (sortDir) {
tempSortDir = `-created`;
}
if (name) {
tempFilter.push(`name ~ "%${name}%"`);
}
if (type) {
tempFilter.push(`type ~ "%${type}%"`);
}
let preFinalListOption = {};
if (tempFilter.length > 0) {
preFinalListOption = { filter: tempFilter.join(' && ') };
}
if (tempSortDir.length > 0) {
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
}
setListOption(preFinalListOption);
// setListOption({
// filter: tempFilter.join(' && '),
// sort: tempSortDir,
// //
// });
}, [visible, sortDir, name, type]);
// return <>helloworld</>;
if (showLoading) return <FormLoading />;
if (showError.show)
return (
<ErrorDisplay
message={t('error.unable-to-process-request')}
code="500"
code={-1}
details={showError.detail}
/>
);
@@ -147,6 +206,9 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
</Card>
</LpCategoriesSelectionProvider>
</Stack>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify(f, null, 2)}</pre>
</Box>
</Box>
);
}

View File

@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list';
import { LpCategory } from '@/components/dashboard/lp_categories/type';
import { LpCategory } from '@/components/dashboard/lp/categories/type';
export default function BasicDetailCard({
lpModel: model,

View File

@@ -10,7 +10,7 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
import { useTranslation } from 'react-i18next';
import { LpCategory } from '@/components/dashboard/lp_categories/type';
import { LpCategory } from '@/components/dashboard/lp/categories/type';
function getImageUrlFrRecord(record: LpCategory): string {
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;

View File

@@ -5,8 +5,8 @@ import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
import SamplePaymentCard from '@/app/dashboard/Sample/SamplePaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
import { COL_QUIZ_LP_QUESTIONS } from '@/constants';
import { Grid } from '@mui/material';
import Box from '@mui/material/Box';
@@ -21,9 +21,9 @@ import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultLpQuestion } from '@/components/dashboard/lp_questions/_constants.ts';
import { Notifications } from '@/components/dashboard/lp_questions/notifications';
import type { LpQuestion } from '@/components/dashboard/lp_questions/type';
import { defaultLpQuestion } from '@/components/dashboard/lp/questions/_constants.ts';
import { Notifications } from '@/components/dashboard/lp/questions/notifications';
import type { LpQuestion } from '@/components/dashboard/lp/questions/type';
import FormLoading from '@/components/loading';
import BasicDetailCard from './BasicDetailCard';

View File

@@ -13,7 +13,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { LpQuestionCreateForm } from '@/components/dashboard/lp_questions/lp-question-create-form';
import { LpQuestionCreateForm } from '@/components/dashboard/lp/questions/lp-question-create-form';
export default function Page(): React.JSX.Element {
// RULES: follow the name of page directory

View File

@@ -10,7 +10,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { LpQuestionEditForm } from '@/components/dashboard/lp_questions/lp-question-edit-form';
import { LpQuestionEditForm } from '@/components/dashboard/lp/questions/lp-question-edit-form';
export default function Page(): React.JSX.Element {
const { t } = useTranslation(['lp_questions']);

View File

@@ -18,17 +18,18 @@ import type { ListResult, RecordModel } from 'pocketbase';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultLpQuestion } from '@/components/dashboard/lp_questions/_constants';
import { LpQuestionsFilters } from '@/components/dashboard/lp_questions/lp-questions-filters';
import type { Filters } from '@/components/dashboard/lp_questions/lp-questions-filters';
import { LpQuestionsPagination } from '@/components/dashboard/lp_questions/lp-questions-pagination';
import { LpQuestionsSelectionProvider } from '@/components/dashboard/lp_questions/lp-questions-selection-context';
import { LpQuestionsTable } from '@/components/dashboard/lp_questions/lp-questions-table';
import type { LpQuestion } from '@/components/dashboard/lp_questions/type';
import { defaultLpQuestion } from '@/components/dashboard/lp/questions/_constants';
import { LpQuestionsFilters } from '@/components/dashboard/lp/questions/lp-questions-filters';
import type { Filters } from '@/components/dashboard/lp/questions/lp-questions-filters';
import { LpQuestionsPagination } from '@/components/dashboard/lp/questions/lp-questions-pagination';
import { LpQuestionsSelectionProvider } from '@/components/dashboard/lp/questions/lp-questions-selection-context';
import { LpQuestionsTable } from '@/components/dashboard/lp/questions/lp-questions-table';
import type { LpQuestion } from '@/components/dashboard/lp/questions/type';
import FormLoading from '@/components/loading';
export default function Page({ searchParams }: PageProps): React.JSX.Element {
@@ -43,8 +44,10 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const [showError, setShowError] = React.useState({ show: false, detail: '' });
//
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
//
const [f, setF] = React.useState<LpQuestion[]>([]);
const [currentPage, setCurrentPage] = React.useState<number>(0);
//
const [recordCount, setRecordCount] = React.useState<number>(0);
const [listOption, setListOption] = React.useState({});
const [listSort, setListSort] = React.useState({});
@@ -202,7 +205,9 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
</Card>
</LpQuestionsSelectionProvider>
</Stack>
<pre>{JSON.stringify(f, null, 2)}</pre>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify(f, null, 2)}</pre>
</Box>
</Box>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,13 +13,13 @@ import { useTranslation } from 'react-i18next';
import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list';
import { LpCategory } from '@/components/dashboard/lp/categories/type';
import type { MfCategory } from '@/components/dashboard/mf/categories/type';
export default function BasicDetailCard({
lpModel: model,
handleEditClick,
}: {
lpModel: LpCategory;
lpModel: MfCategory;
handleEditClick: () => void;
}): React.JSX.Element {
const { t } = useTranslation();

View File

@@ -10,13 +10,13 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
import { useTranslation } from 'react-i18next';
import { LpCategory } from '@/components/dashboard/lp/categories/type';
import type { MfCategory } from '@/components/dashboard/mf/categories/type';
function getImageUrlFrRecord(record: LpCategory): string {
function getImageUrlFrRecord(record: MfCategory): string {
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
}
export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element {
export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element {
const { t } = useTranslation();
return (

View File

@@ -5,9 +5,9 @@ import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
import SamplePaymentCard from '@/app/dashboard/Sample/SamplePaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
import { COL_QUIZ_MF_CATEGORIES } from '@/constants';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
@@ -21,9 +21,9 @@ import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants.ts';
import { Notifications } from '@/components/dashboard/lp/categories/notifications';
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
import { defaultMfCategory } from '@/components/dashboard/mf/categories/_constants.ts';
import { Notifications } from '@/components/dashboard/mf/categories/notifications';
import type { MfCategory } from '@/components/dashboard/mf/categories/type';
import FormLoading from '@/components/loading';
import BasicDetailCard from './BasicDetailCard';
@@ -39,18 +39,18 @@ export default function Page(): React.JSX.Element {
const [showError, setShowError] = React.useState({ show: false, detail: '' });
//
const [showLessonCategory, setShowLessonCategory] = React.useState<LpCategory>(defaultLpCategory);
const [showLessonCategory, setShowLessonCategory] = React.useState<MfCategory>(defaultMfCategory);
function handleEditClick() {
router.push(paths.dashboard.lp_categories.edit(showLessonCategory.id));
router.push(paths.dashboard.mf_categories.edit(showLessonCategory.id));
}
React.useEffect(() => {
if (catId) {
pb.collection(COL_QUIZ_LP_CATEGORIES)
pb.collection(COL_QUIZ_MF_CATEGORIES)
.getOne(catId)
.then((model: RecordModel) => {
setShowLessonCategory({ ...defaultLpCategory, ...model });
setShowLessonCategory({ ...defaultMfCategory, ...model });
})
.catch((err) => {
logger.error(err);
@@ -89,7 +89,7 @@ export default function Page(): React.JSX.Element {
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.lp_categories.list}
href={paths.dashboard.mf_categories.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>

View File

@@ -13,7 +13,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { LpCategoryCreateForm } from '@/components/dashboard/lp/categories/lp-category-create-form';
import { MfCategoryCreateForm } from '@/components/dashboard/mf/categories/mf-category-create-form';
export default function Page(): React.JSX.Element {
// RULES: follow the name of page directory
@@ -34,7 +34,7 @@ export default function Page(): React.JSX.Element {
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.lp_categories.list}
href={paths.dashboard.mf_categories.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
@@ -46,7 +46,7 @@ export default function Page(): React.JSX.Element {
<Typography variant="h4">{t('create.title')}</Typography>
</div>
</Stack>
<LpCategoryCreateForm />
<MfCategoryCreateForm />
</Stack>
</Box>
);

View File

@@ -10,7 +10,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { LpCategoryEditForm } from '@/components/dashboard/lp/categories/lp-category-edit-form';
import { MfCategoryEditForm } from '@/components/dashboard/mf/categories/mf-category-edit-form';
export default function Page(): React.JSX.Element {
const { t } = useTranslation(['lp_categories']);
@@ -34,7 +34,7 @@ export default function Page(): React.JSX.Element {
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.lp_categories.list}
href={paths.dashboard.mf_categories.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
@@ -46,7 +46,7 @@ export default function Page(): React.JSX.Element {
<Typography variant="h4">{t('edit.title')}</Typography>
</div>
</Stack>
<LpCategoryEditForm />
<MfCategoryEditForm />
</Stack>
</Box>
);

View File

@@ -6,7 +6,7 @@
//
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import { COL_QUIZ_MF_CATEGORIES } from '@/constants';
import { LoadingButton } from '@mui/lab';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
@@ -18,24 +18,25 @@ import type { ListResult, RecordModel } from 'pocketbase';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants';
import { LpCategoriesFilters } from '@/components/dashboard/lp/categories/lp-categories-filters';
import type { Filters } from '@/components/dashboard/lp/categories/lp-categories-filters';
import { LpCategoriesPagination } from '@/components/dashboard/lp/categories/lp-categories-pagination';
import { LpCategoriesSelectionProvider } from '@/components/dashboard/lp/categories/lp-categories-selection-context';
import { LpCategoriesTable } from '@/components/dashboard/lp/categories/lp-categories-table';
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
import { defaultMfCategory } from '@/components/dashboard/mf/categories/_constants';
import { MfCategoriesFilters } from '@/components/dashboard/mf/categories/mf-categories-filters';
import type { Filters } from '@/components/dashboard/mf/categories/mf-categories-filters';
import { MfCategoriesPagination } from '@/components/dashboard/mf/categories/mf-categories-pagination';
import { MfCategoriesSelectionProvider } from '@/components/dashboard/mf/categories/mf-categories-selection-context';
import { MfCategoriesTable } from '@/components/dashboard/mf/categories/mf-categories-table';
import type { MfCategory } from '@/components/dashboard/mf/categories/type';
import FormLoading from '@/components/loading';
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { t } = useTranslation(['lp_categories']);
const { t } = useTranslation(['mf_categories']);
const { email, phone, sortDir, status, name, visible, type } = searchParams;
const router = useRouter();
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<LpCategory[]>([]);
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<MfCategory[]>([]);
//
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
@@ -43,7 +44,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const [showError, setShowError] = React.useState({ show: false, detail: '' });
//
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
const [f, setF] = React.useState<LpCategory[]>([]);
const [f, setF] = React.useState<MfCategory[]>([]);
const [currentPage, setCurrentPage] = React.useState<number>(1);
const [recordCount, setRecordCount] = React.useState<number>(0);
const [listOption, setListOption] = React.useState({});
@@ -56,36 +57,91 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const reloadRows = async (): Promise<void> => {
try {
const models: ListResult<RecordModel> = await pb
.collection(COL_QUIZ_LP_CATEGORIES)
.getList(currentPage + 1, rowsPerPage, {});
.collection(COL_QUIZ_MF_CATEGORIES)
.getList(currentPage + 1, rowsPerPage, listOption);
const { items, totalItems } = models;
const tempLessonTypes: LpCategory[] = items.map((lt) => {
return { ...defaultLpCategory, ...lt };
const tempLessonTypes: MfCategory[] = items.map((lt) => {
return { ...defaultMfCategory, ...lt };
});
setLessonCategoriesData(tempLessonTypes);
setRecordCount(totalItems);
setF(tempLessonTypes);
console.log({ currentPage, f });
// console.log({ currentPage, f });
} catch (error) {
//
setShowError({ show: true, detail: JSON.stringify(error) });
logger.error(error);
setShowError({
//
show: true,
detail: JSON.stringify(error, null, 2),
});
} finally {
setShowLoading(false);
}
};
const [lastListOption, setLastListOption] = React.useState({});
const isFirstRun = React.useRef(false);
React.useEffect(() => {
void reloadRows();
if (!isFirstRun.current) {
isFirstRun.current = true;
} else {
if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
// reset page number as tab changes
setLastListOption(listOption);
setCurrentPage(0);
void reloadRows();
} else {
void reloadRows();
}
}
}, [currentPage, rowsPerPage, listOption]);
React.useEffect(() => {
let tempFilter = [],
tempSortDir = '';
if (visible) {
tempFilter.push(`visible = "${visible}"`);
}
if (sortDir) {
tempSortDir = `-created`;
}
if (name) {
tempFilter.push(`name ~ "%${name}%"`);
}
if (type) {
tempFilter.push(`type ~ "%${type}%"`);
}
let preFinalListOption = {};
if (tempFilter.length > 0) {
preFinalListOption = { filter: tempFilter.join(' && ') };
}
if (tempSortDir.length > 0) {
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
}
setListOption(preFinalListOption);
// setListOption({
// filter: tempFilter.join(' && '),
// sort: tempSortDir,
// //
// });
}, [visible, sortDir, name, type]);
// return <>helloworld</>;
if (showLoading) return <FormLoading />;
if (showError.show)
return (
<ErrorDisplay
message={t('error.unable-to-process-request')}
code="500"
code={-1}
details={showError.detail}
/>
);
@@ -113,7 +169,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
loading={isLoadingAddPage}
onClick={(): void => {
setIsLoadingAddPage(true);
router.push(paths.dashboard.lp_categories.create);
router.push(paths.dashboard.mf_categories.create);
}}
startIcon={<PlusIcon />}
variant="contained"
@@ -122,22 +178,22 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
</LoadingButton>
</Box>
</Stack>
<LpCategoriesSelectionProvider lessonCategories={f}>
<MfCategoriesSelectionProvider lessonCategories={f}>
<Card>
<LpCategoriesFilters
<MfCategoriesFilters
filters={{ email, phone, status, name, visible, type }}
fullData={lessonCategoriesData}
sortDir={sortDir}
/>
<Divider />
<Box sx={{ overflowX: 'auto' }}>
<LpCategoriesTable
<MfCategoriesTable
reloadRows={reloadRows}
rows={f}
/>
</Box>
<Divider />
<LpCategoriesPagination
<MfCategoriesPagination
count={recordCount}
page={currentPage}
rowsPerPage={rowsPerPage}
@@ -145,15 +201,18 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
setRowsPerPage={setRowsPerPage}
/>
</Card>
</LpCategoriesSelectionProvider>
</MfCategoriesSelectionProvider>
</Stack>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify(f, null, 2)}</pre>
</Box>
</Box>
);
}
// Sorting and filtering has to be done on the server.
function applySort(row: LpCategory[], sortDir: 'asc' | 'desc' | undefined): LpCategory[] {
function applySort(row: MfCategory[], sortDir: 'asc' | 'desc' | undefined): MfCategory[] {
return row.sort((a, b) => {
if (sortDir === 'asc') {
return a.createdAt.getTime() - b.createdAt.getTime();
@@ -163,7 +222,7 @@ function applySort(row: LpCategory[], sortDir: 'asc' | 'desc' | undefined): LpCa
});
}
function applyFilters(row: LpCategory[], { email, phone, status, name, visible }: Filters): LpCategory[] {
function applyFilters(row: MfCategory[], { email, phone, status, name, visible }: Filters): MfCategory[] {
return row.filter((item) => {
if (email) {
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {

View File

@@ -13,13 +13,13 @@ import { useTranslation } from 'react-i18next';
import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list';
import { LpCategory } from '@/components/dashboard/lp/categories/type';
import { MfCategory } from '@/components/dashboard/mf/categories/type';
export default function BasicDetailCard({
lpModel: model,
handleEditClick,
}: {
lpModel: LpCategory;
lpModel: MfCategory;
handleEditClick: () => void;
}): React.JSX.Element {
const { t } = useTranslation();

View File

@@ -10,13 +10,13 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
import { useTranslation } from 'react-i18next';
import { LpCategory } from '@/components/dashboard/lp/categories/type';
import type { MfCategory } from '@/components/dashboard/mf/categories/type';
function getImageUrlFrRecord(record: LpCategory): string {
function getImageUrlFrRecord(record: MfCategory): string {
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
}
export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element {
export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element {
const { t } = useTranslation();
return (

View File

@@ -5,9 +5,9 @@ import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
import SamplePaymentCard from '@/app/dashboard/Sample/SamplePaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
import { COL_QUIZ_LP_QUESTIONS } from '@/constants';
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
import { COL_QUIZ_MF_QUESTIONS } from '@/constants';
import { Grid } from '@mui/material';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
@@ -21,9 +21,9 @@ import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultLpQuestion } from '@/components/dashboard/lp/questions/_constants.ts';
import { Notifications } from '@/components/dashboard/lp/questions/notifications';
import type { LpQuestion } from '@/components/dashboard/lp/questions/type';
import { defaultMfQuestion } from '@/components/dashboard/mf/questions/_constants.ts';
import { Notifications } from '@/components/dashboard/mf/questions/notifications';
import type { MfQuestion } from '@/components/dashboard/mf/questions/type';
import FormLoading from '@/components/loading';
import BasicDetailCard from './BasicDetailCard';
@@ -39,18 +39,18 @@ export default function Page(): React.JSX.Element {
const [showError, setShowError] = React.useState({ show: false, detail: '' });
//
const [showLessonQuestion, setShowLessonQuestion] = React.useState<LpQuestion>(defaultLpQuestion);
const [showLessonQuestion, setShowLessonQuestion] = React.useState<MfQuestion>(defaultMfQuestion);
function handleEditClick() {
router.push(paths.dashboard.lp_questions.edit(showLessonQuestion.id));
router.push(paths.dashboard.mf_questions.edit(showLessonQuestion.id));
}
React.useEffect(() => {
if (catId) {
pb.collection(COL_QUIZ_LP_QUESTIONS)
pb.collection(COL_QUIZ_MF_QUESTIONS)
.getOne(catId)
.then((model: RecordModel) => {
setShowLessonQuestion({ ...defaultLpQuestion, ...model });
setShowLessonQuestion({ ...defaultMfQuestion, ...model });
})
.catch((err) => {
logger.error(err);
@@ -89,7 +89,7 @@ export default function Page(): React.JSX.Element {
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.lp_questions.list}
href={paths.dashboard.mf_questions.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>

View File

@@ -13,7 +13,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { LpQuestionCreateForm } from '@/components/dashboard/lp/questions/lp-question-create-form';
import { MfQuestionCreateForm } from '@/components/dashboard/mf/questions/mf-question-create-form';
export default function Page(): React.JSX.Element {
// RULES: follow the name of page directory
@@ -34,7 +34,7 @@ export default function Page(): React.JSX.Element {
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.lp_questions.list}
href={paths.dashboard.mf_questions.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
@@ -46,7 +46,7 @@ export default function Page(): React.JSX.Element {
<Typography variant="h4">{t('create.title')}</Typography>
</div>
</Stack>
<LpQuestionCreateForm />
<MfQuestionCreateForm />
</Stack>
</Box>
);

View File

@@ -10,7 +10,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { LpQuestionEditForm } from '@/components/dashboard/lp/questions/lp-question-edit-form';
import { MfQuestionEditForm } from '@/components/dashboard/mf/questions/mf-question-edit-form';
export default function Page(): React.JSX.Element {
const { t } = useTranslation(['lp_questions']);
@@ -34,7 +34,7 @@ export default function Page(): React.JSX.Element {
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.lp_questions.list}
href={paths.dashboard.mf_questions.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
@@ -46,7 +46,7 @@ export default function Page(): React.JSX.Element {
<Typography variant="h4">{t('edit.title')}</Typography>
</div>
</Stack>
<LpQuestionEditForm />
<MfQuestionEditForm />
</Stack>
</Box>
);

View File

@@ -6,7 +6,7 @@
//
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { COL_QUIZ_LP_QUESTIONS } from '@/constants';
import { COL_QUIZ_MF_QUESTIONS } from '@/constants';
import { LoadingButton } from '@mui/lab';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
@@ -18,24 +18,25 @@ import type { ListResult, RecordModel } from 'pocketbase';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultLpQuestion } from '@/components/dashboard/lp/questions/_constants';
import { LpQuestionsFilters } from '@/components/dashboard/lp/questions/lp-questions-filters';
import type { Filters } from '@/components/dashboard/lp/questions/lp-questions-filters';
import { LpQuestionsPagination } from '@/components/dashboard/lp/questions/lp-questions-pagination';
import { LpQuestionsSelectionProvider } from '@/components/dashboard/lp/questions/lp-questions-selection-context';
import { LpQuestionsTable } from '@/components/dashboard/lp/questions/lp-questions-table';
import type { LpQuestion } from '@/components/dashboard/lp/questions/type';
import { defaultMfQuestion } from '@/components/dashboard/mf/questions/_constants';
import { MfQuestionsFilters } from '@/components/dashboard/mf/questions/mf-questions-filters';
import type { Filters } from '@/components/dashboard/mf/questions/mf-questions-filters';
import { MfQuestionsPagination } from '@/components/dashboard/mf/questions/mf-questions-pagination';
import { MfQuestionsSelectionProvider } from '@/components/dashboard/mf/questions/mf-questions-selection-context';
import { MfQuestionsTable } from '@/components/dashboard/mf/questions/mf-questions-table';
import type { MfQuestion } from '@/components/dashboard/mf/questions/type';
import FormLoading from '@/components/loading';
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { t } = useTranslation(['lp_questions']);
const { t } = useTranslation(['mf_question']);
const { email, phone, sortDir, status, name, visible, type } = searchParams;
const router = useRouter();
const [lessonQuestionsData, setLessonCategoriesData] = React.useState<LpQuestion[]>([]);
const [lessonQuestionsData, setLessonCategoriesData] = React.useState<MfQuestion[]>([]);
//
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
@@ -43,7 +44,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const [showError, setShowError] = React.useState({ show: false, detail: '' });
//
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
const [f, setF] = React.useState<LpQuestion[]>([]);
const [f, setF] = React.useState<MfQuestion[]>([]);
const [currentPage, setCurrentPage] = React.useState<number>(0);
const [recordCount, setRecordCount] = React.useState<number>(0);
const [listOption, setListOption] = React.useState({});
@@ -56,11 +57,11 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const reloadRows = async (): Promise<void> => {
try {
const models: ListResult<RecordModel> = await pb
.collection(COL_QUIZ_LP_QUESTIONS)
.collection(COL_QUIZ_MF_QUESTIONS)
.getList(currentPage + 1, rowsPerPage, listOption);
const { items, totalItems } = models;
const tempLessonTypes: LpQuestion[] = items.map((lt) => {
return { ...defaultLpQuestion, ...lt };
const tempLessonTypes: MfQuestion[] = items.map((lt) => {
return { ...defaultMfQuestion, ...lt };
});
setLessonCategoriesData(tempLessonTypes);
@@ -177,22 +178,22 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
</LoadingButton>
</Box>
</Stack>
<LpQuestionsSelectionProvider lessonQuestions={f}>
<MfQuestionsSelectionProvider lessonQuestions={f}>
<Card>
<LpQuestionsFilters
<MfQuestionsFilters
filters={{ email, phone, status, name, visible, type }}
fullData={lessonQuestionsData}
sortDir={sortDir}
/>
<Divider />
<Box sx={{ overflowX: 'auto' }}>
<LpQuestionsTable
<MfQuestionsTable
reloadRows={reloadRows}
rows={f}
/>
</Box>
<Divider />
<LpQuestionsPagination
<MfQuestionsPagination
count={recordCount}
page={currentPage}
rowsPerPage={rowsPerPage}
@@ -200,16 +201,18 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
setRowsPerPage={setRowsPerPage}
/>
</Card>
</LpQuestionsSelectionProvider>
</MfQuestionsSelectionProvider>
</Stack>
<pre>{JSON.stringify(f, null, 2)}</pre>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify(f, null, 2)}</pre>
</Box>
</Box>
);
}
// Sorting and filtering has to be done on the server.
function applySort(row: LpQuestion[], sortDir: 'asc' | 'desc' | undefined): LpQuestion[] {
function applySort(row: MfQuestion[], sortDir: 'asc' | 'desc' | undefined): MfQuestion[] {
return row.sort((a, b) => {
if (sortDir === 'asc') {
return a.createdAt.getTime() - b.createdAt.getTime();
@@ -219,7 +222,7 @@ function applySort(row: LpQuestion[], sortDir: 'asc' | 'desc' | undefined): LpQu
});
}
function applyFilters(row: LpQuestion[], { email, phone, status, name, visible }: Filters): LpQuestion[] {
function applyFilters(row: MfQuestion[], { email, phone, status, name, visible }: Filters): MfQuestion[] {
return row.filter((item) => {
if (email) {
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,8 @@
'use client';
// lesson-categories-pagination.tsx
// RULES:
// T.B.A.
import * as React from 'react';
import TablePagination from '@mui/material/TablePagination';
@@ -10,20 +13,39 @@ function noop(): void {
interface LessonCategoriesPaginationProps {
count: number;
page: number;
//
setPage: (page: number) => void;
setRowsPerPage: (page: number) => void;
rowsPerPage: number;
}
export function LessonCategoriesPagination({ count, page }: LessonCategoriesPaginationProps): React.JSX.Element {
export function LessonCategoriesPagination({
count,
page,
//
setPage,
setRowsPerPage,
rowsPerPage,
}: LessonCategoriesPaginationProps): React.JSX.Element {
// You should implement the pagination using a similar logic as the filters.
// Note that when page change, you should keep the filter search params.
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value));
// console.log(parseInt(event.target.value));
};
return (
<TablePagination
component="div"
count={count}
onPageChange={noop}
onRowsPerPageChange={noop}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
page={page}
rowsPerPage={5}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[5, 10, 25]}
/>
);

View File

@@ -1,5 +1,8 @@
'use client';
// lp-categories-pagination.tsx
// RULES:
// T.B.A.
import * as React from 'react';
import TablePagination from '@mui/material/TablePagination';
@@ -7,7 +10,7 @@ function noop(): void {
return undefined;
}
interface LessonCategoriesPaginationProps {
interface LpCategoriesPaginationProps {
count: number;
page: number;
//
@@ -23,7 +26,7 @@ export function LpCategoriesPagination({
setPage,
setRowsPerPage,
rowsPerPage,
}: LessonCategoriesPaginationProps): React.JSX.Element {
}: LpCategoriesPaginationProps): React.JSX.Element {
// You should implement the pagination using a similar logic as the filters.
// Note that when page change, you should keep the filter search params.
const handleChangePage = (event: unknown, newPage: number) => {

View File

@@ -1 +0,0 @@
please review and add translations, e.g. `{t('[word]')}`

View File

@@ -1,44 +0,0 @@
import { dayjs } from '@/lib/dayjs';
import { CreateFormProps, LpCategory } from './type';
export const defaultLpCategory: LpCategory = {
isEmpty: false,
id: 'default-id',
cat_name: 'default-category-name',
cat_image_url: undefined,
cat_image: undefined,
pos: 0,
visible: 'hidden',
lesson_id: 'default-lesson-id',
description: 'default-description',
remarks: 'default-remarks',
slug: '',
init_answer: {},
// from pocketbase
collectionId: '0000000000',
createdAt: dayjs('2099-01-01').toDate(),
//
name: '',
avatar: '',
email: '',
phone: '',
quota: 0,
status: 'NA',
};
// export const LpCategoryCreateFormDefault: CreateFormProps = {
// name: '',
// type: '',
// pos: 1,
// visible: 'visible',
// description: '',
// isActive: true,
// order: 1,
// imageUrl: '',
// };
export const emptyLpCategory: LpCategory = {
...defaultLpCategory,
isEmpty: true,
};

View File

@@ -1,125 +0,0 @@
'use client';
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { COL_LESSON_TYPES } from '@/constants';
import deleteQuizLPCategories from '@/db/QuizListenings/Delete';
import { LoadingButton } from '@mui/lab';
import { Button, Container, Modal, Paper } from '@mui/material';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note';
import PocketBase from 'pocketbase';
import { useTranslation } from 'react-i18next';
import { logger } from '@/lib/default-logger';
import { toast } from '@/components/core/toaster';
export default function ConfirmDeleteModal({
open,
setOpen,
idToDelete,
reloadRows,
}: {
open: boolean;
setOpen: (b: boolean) => void;
idToDelete: string;
reloadRows: () => void;
}): React.JSX.Element {
const { t } = useTranslation();
// const handleClose = () => setOpen(false);
function handleClose(): void {
setOpen(false);
}
const [isDeleteing, setIsDeleteing] = React.useState(false);
const style = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
};
function handleUserConfirmDelete(): void {
if (idToDelete) {
setIsDeleteing(true);
deleteQuizLPCategories(idToDelete)
.then(() => {
reloadRows();
handleClose();
toast(t('delete.success'));
})
.catch((err) => {
// console.error(err)
logger.error(err);
toast(t('delete.error'));
})
.finally(() => {
setIsDeleteing(false);
});
}
}
return (
<div>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<Container maxWidth="sm">
<Paper sx={{ border: '1px solid var(--mui-palette-divider)', boxShadow: 'var(--mui-shadows-16)' }}>
<Stack
direction="row"
spacing={2}
sx={{ display: 'flex', p: 3 }}
>
<Avatar sx={{ bgcolor: 'var(--mui-palette-error-50)', color: 'var(--mui-palette-error-main)' }}>
<NoteIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
<Stack spacing={3}>
<Stack spacing={1}>
<Typography variant="h5">{t('Delete Lesson Type ?')}</Typography>
<Typography
color="text.secondary"
variant="body2"
>
{t('Are you sure you want to delete lesson type ?')}
</Typography>
</Stack>
<Stack
direction="row"
spacing={2}
sx={{ justifyContent: 'flex-end' }}
>
<Button
color="secondary"
onClick={handleClose}
>
{t('Cancel')}
</Button>
<LoadingButton
color="error"
variant="contained"
onClick={(e) => {
handleUserConfirmDelete();
}}
loading={isDeleteing}
>
{t('Delete')}
</LoadingButton>
</Stack>
</Stack>
</Stack>
</Paper>
</Container>
</Box>
</Modal>
</div>
);
}

View File

@@ -1,49 +0,0 @@
'use client';
import * as React from 'react';
import TablePagination from '@mui/material/TablePagination';
function noop(): void {
return undefined;
}
interface LessonCategoriesPaginationProps {
count: number;
page: number;
//
setPage: (page: number) => void;
setRowsPerPage: (page: number) => void;
rowsPerPage: number;
}
export function LpCategoriesPagination({
count,
page,
//
setPage,
setRowsPerPage,
rowsPerPage,
}: LessonCategoriesPaginationProps): React.JSX.Element {
// You should implement the pagination using a similar logic as the filters.
// Note that when page change, you should keep the filter search params.
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value));
// console.log(parseInt(event.target.value));
};
return (
<TablePagination
component="div"
count={count}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
page={page}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[5, 10, 25]}
/>
);
}

View File

@@ -1,500 +0,0 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
//
import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
import Divider from '@mui/material/Divider';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2';
//
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
//
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
import { TextEditor } from '@/components/core/text-editor/text-editor';
import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading';
import ErrorDisplay from '../error';
import type { EditFormProps } from './type';
// TODO: review this
const schema = zod.object({
cat_name: zod.string().min(1, 'name-is-required').max(255),
// accept file object when user change image
// accept http string when user not changing image
cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(),
// position
pos: zod.number().min(1, 'position is required').max(99),
// it should be a valid JSON
init_answer: zod
.string()
.refine(
(value) => {
try {
JSON.parse(value);
return true;
} catch (error) {
return false;
}
},
{ message: 'init_answer must be a valid JSON' }
)
.optional(),
visible: zod.string(),
slug: zod.string().min(0, 'slug-is-required').max(255).optional(),
remarks: zod.string().optional(),
description: zod.string().optional(),
// NOTE: for image handling
avatar: zod.string().optional(),
});
type Values = zod.infer<typeof schema>;
const defaultValues = {
cat_name: '',
cat_image: undefined,
pos: 1,
init_answer: JSON.stringify({}),
visible: 'hidden',
slug: '',
remarks: '',
description: '',
} satisfies Values;
export function LpCategoryEditForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_categories']);
const { cat_id: catId } = useParams<{ cat_id: string }>();
//
const [isUpdating, setIsUpdating] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(false);
//
const [showError, setShowError] = React.useState({ show: false, detail: '' });
const {
control,
handleSubmit,
formState: { errors },
setValue,
reset,
watch,
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
setIsUpdating(true);
const tempUpdate: EditFormProps = {
cat_name: values.cat_name,
cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null,
pos: values.pos,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
init_answer: JSON.parse(values.init_answer || '{}'),
visible: values.visible,
slug: values.slug || 'not-defined',
remarks: values.remarks,
description: values.description,
//
// TODO: remove below
type: '',
};
try {
const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate);
logger.debug(result);
toast.success(t('edit.success'));
router.push(paths.dashboard.lp_categories.list);
} catch (error) {
logger.error(error);
toast.error(t('update.failed'));
} finally {
setIsUpdating(false);
}
},
// t is not necessary here
// eslint-disable-next-line react-hooks/exhaustive-deps
[router]
);
const avatarInputRef = React.useRef<HTMLInputElement>(null);
const avatar = watch('avatar');
const handleAvatarChange = React.useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const url = await fileToBase64(file);
setValue('avatar', url);
}
},
[setValue]
);
// TODO: need to align with save form
// use trycatch
const [textDescription, setTextDescription] = React.useState<string>('');
const [textRemarks, setTextRemarks] = React.useState<string>('');
// load existing data when user arrive
const loadExistingData = React.useCallback(
async (id: string) => {
setShowLoading(true);
try {
const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id);
reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) });
setTextDescription(result.description);
setTextRemarks(result.remarks);
if (result.cat_image !== '') {
const fetchResult = await fetch(
`http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}`
);
const blob = await fetchResult.blob();
const url = await fileToBase64(blob);
setValue('avatar', url);
} else {
setValue('avatar', '');
}
} catch (error) {
logger.error(error);
toast(t('list.error'));
setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
} finally {
setShowLoading(false);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[catId]
);
React.useEffect(() => {
void loadExistingData(catId);
// 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 (
<form onSubmit={handleSubmit(onSubmit)}>
<Card>
<CardContent>
<Stack
divider={<Divider />}
spacing={4}
>
<Stack spacing={3}>
<Typography variant="h6">{t('edit.basic-info')}</Typography>
<Grid
container
spacing={3}
>
<Grid xs={12}>
<Stack
direction="row"
spacing={3}
sx={{ alignItems: 'center' }}
>
<Box
sx={{
border: '1px dashed var(--mui-palette-divider)',
borderRadius: '5%',
display: 'inline-flex',
p: '4px',
}}
>
<Avatar
variant="rounded"
src={avatar}
sx={{
'--Avatar-size': '100px',
'--Icon-fontSize': 'var(--icon-fontSize-lg)',
alignItems: 'center',
bgcolor: 'var(--mui-palette-background-level1)',
color: 'var(--mui-palette-text-primary)',
display: 'flex',
justifyContent: 'center',
}}
>
<CameraIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
</Box>
<Stack
spacing={1}
sx={{ alignItems: 'flex-start' }}
>
<Typography variant="subtitle1">{t('edit.avatar')}</Typography>
<Typography variant="caption">{t('edit.avatarRequirements')}</Typography>
<Button
color="secondary"
onClick={() => {
avatarInputRef.current?.click();
}}
variant="outlined"
>
{t('edit.avatar_select')}
</Button>
<input
hidden
onChange={handleAvatarChange}
ref={avatarInputRef}
type="file"
/>
</Stack>
</Stack>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="cat_name"
render={({ field }) => (
<FormControl
disabled={isUpdating}
error={Boolean(errors.cat_name)}
fullWidth
>
<InputLabel required>{t('edit.cat_name')}</InputLabel>
<OutlinedInput {...field} />
{errors.cat_name ? <FormHelperText>{errors.cat_name.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="pos"
render={({ field }) => (
<FormControl
error={Boolean(errors.pos)}
fullWidth
>
<InputLabel required>{t('edit.pos')}</InputLabel>
<OutlinedInput
{...field}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
type="number"
/>
{errors.pos ? <FormHelperText>{errors.pos.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="slug"
render={({ field }) => (
<FormControl
error={Boolean(errors.slug)}
fullWidth
>
<InputLabel required>{t('edit.slug')}</InputLabel>
<OutlinedInput {...field} />
{errors.slug ? <FormHelperText>{errors.slug.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="visible"
render={({ field }) => (
<FormControl
error={Boolean(errors.visible)}
fullWidth
>
<InputLabel>{t('edit.visible')}</InputLabel>
<Select {...field}>
<MenuItem value="visible">{t('edit.visible')}</MenuItem>
<MenuItem value="hidden">{t('edit.hidden')}</MenuItem>
</Select>
{errors.visible ? <FormHelperText>{errors.visible.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="init_answer"
render={({ field }) => (
<FormControl
error={Boolean(errors.init_answer)}
fullWidth
>
<InputLabel required>{t('edit.init_answer')}</InputLabel>
<OutlinedInput {...field} />
{errors.init_answer ? <FormHelperText>{errors.init_answer.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
{/* */}
<Stack spacing={3}>
<Typography variant="h6">{t('edit.detail-information')}</Typography>
<Grid
container
spacing={3}
>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="description"
render={({ field }) => {
return (
<Box>
<Typography
variant="subtitle1"
color="text-secondary"
>
{t('edit.description')}
</Typography>
<Box sx={{ mt: '8px', '& .tiptap-container': { height: '200px' } }}>
<TextEditor
{...field}
content={textDescription}
onUpdate={({ editor }) => {
field.onChange({ target: { value: editor.getHTML() } });
}}
placeholder={t('edit.description.default')}
/>
</Box>
</Box>
);
}}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="remarks"
render={({ field }) => (
<Box>
<Typography
variant="subtitle1"
color="text.secondary"
>
{t('edit.remarks')}
</Typography>
<Box sx={{ mt: '8px', '& .tiptap-container': { height: '200px' } }}>
<TextEditor
content={textRemarks}
onUpdate={({ editor }) => {
field.onChange({ target: { value: editor.getText() } });
}}
hideToolbar
placeholder={t('edit.remarks.default')}
/>
</Box>
</Box>
)}
/>
</Grid>
</Grid>
</Stack>
{/* */}
</Stack>
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}>
<Button
color="secondary"
component={RouterLink}
href={paths.dashboard.lp_categories.list}
>
{t('edit.cancelButton')}
</Button>
<LoadingButton
disabled={isUpdating}
loading={isUpdating}
type="submit"
variant="contained"
>
{t('edit.updateButton')}
</LoadingButton>
</CardActions>
</Card>
</form>
);
}

View File

@@ -1,101 +0,0 @@
'use client';
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Chip from '@mui/material/Chip';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple';
import { dayjs } from '@/lib/dayjs';
import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table';
import { Option } from '@/components/core/option';
export interface Notification {
id: string;
type: string;
status: 'delivered' | 'pending' | 'failed';
createdAt: Date;
}
const columns = [
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="inherit">
{row.type}
</Typography>
),
name: 'Type',
width: '300px',
},
{
formatter: (row): React.JSX.Element => {
const mapping = {
delivered: { label: 'Delivered', color: 'success' },
pending: { label: 'Pending', color: 'warning' },
failed: { label: 'Failed', color: 'error' },
} as const;
const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' };
return <Chip color={color} label={label} size="small" variant="soft" />;
},
name: 'Status',
width: '200px',
},
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="inherit">
{dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')}
</Typography>
),
name: 'Date',
align: 'right',
},
] satisfies ColumnDef<Notification>[];
export interface NotificationsProps {
notifications: Notification[];
}
export function Notifications({ notifications }: NotificationsProps): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<EnvelopeSimpleIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Notifications"
/>
<CardContent>
<Stack spacing={3}>
<Stack spacing={2}>
<Select defaultValue="last_invoice" name="type" sx={{ maxWidth: '100%', width: '320px' }}>
<Option value="last_invoice">Resend last invoice</Option>
<Option value="password_reset">Send password reset</Option>
<Option value="verification">Send verification</Option>
</Select>
<div>
<Button startIcon={<EnvelopeSimpleIcon />} variant="contained">
Send email
</Button>
</div>
</Stack>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<Box sx={{ overflowX: 'auto' }}>
<DataTable<Notification> columns={columns} rows={notifications} />
</Box>
</Card>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -1,138 +0,0 @@
'use client';
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple';
import { dayjs } from '@/lib/dayjs';
import type { ColumnDef } from '@/components/core/data-table';
import { DataTable } from '@/components/core/data-table';
export interface Payment {
currency: string;
amount: number;
invoiceId: string;
status: 'pending' | 'completed' | 'canceled' | 'refunded';
createdAt: Date;
}
const columns = [
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="subtitle2">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)}
</Typography>
),
name: 'Amount',
width: '200px',
},
{
formatter: (row): React.JSX.Element => {
const mapping = {
pending: { label: 'Pending', color: 'warning' },
completed: { label: 'Completed', color: 'success' },
canceled: { label: 'Canceled', color: 'error' },
refunded: { label: 'Refunded', color: 'error' },
} as const;
const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' };
return <Chip color={color} label={label} size="small" variant="soft" />;
},
name: 'Status',
width: '200px',
},
{
formatter: (row): React.JSX.Element => {
return <Link variant="inherit">{row.invoiceId}</Link>;
},
name: 'Invoice ID',
width: '150px',
},
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="inherit">
{dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')}
</Typography>
),
name: 'Date',
align: 'right',
},
] satisfies ColumnDef<Payment>[];
export interface PaymentsProps {
ordersValue: number;
payments: Payment[];
refundsValue: number;
totalOrders: number;
}
export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element {
return (
<Card>
<CardHeader
action={
<Button color="secondary" startIcon={<PlusIcon />}>
Create Payment
</Button>
}
avatar={
<Avatar>
<ShoppingCartSimpleIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Payments"
/>
<CardContent>
<Stack spacing={3}>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<Stack
direction="row"
divider={<Divider flexItem orientation="vertical" />}
spacing={3}
sx={{ justifyContent: 'space-between', p: 2 }}
>
<div>
<Typography color="text.secondary" variant="overline">
Total orders
</Typography>
<Typography variant="h6">{new Intl.NumberFormat('en-US').format(totalOrders)}</Typography>
</div>
<div>
<Typography color="text.secondary" variant="overline">
Orders value
</Typography>
<Typography variant="h6">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)}
</Typography>
</div>
<div>
<Typography color="text.secondary" variant="overline">
Refunds
</Typography>
<Typography variant="h6">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)}
</Typography>
</div>
</Stack>
</Card>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<Box sx={{ overflowX: 'auto' }}>
<DataTable<Payment> columns={columns} rows={payments} />
</Box>
</Card>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -1,46 +0,0 @@
import * as React from 'react';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Chip from '@mui/material/Chip';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
export interface Address {
id: string;
country: string;
state: string;
city: string;
zipCode: string;
street: string;
primary?: boolean;
}
export interface ShippingAddressProps {
address: Address;
}
export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement {
return (
<Card sx={{ borderRadius: 1, height: '100%' }} variant="outlined">
<CardContent>
<Stack spacing={2}>
<Typography>
{address.street},
<br />
{address.city}, {address.state}, {address.country},
<br />
{address.zipCode}
</Typography>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'space-between' }}>
{address.primary ? <Chip color="warning" label="Primary" variant="soft" /> : <span />}
<Button color="secondary" size="small" startIcon={<PencilSimpleIcon />}>
Edit
</Button>
</Stack>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -1,61 +0,0 @@
export interface LpCategory {
isEmpty?: boolean;
//
id: string;
collectionId: string;
//
cat_name: string;
cat_image_url?: string;
cat_image?: string;
pos: number;
visible: string;
lesson_id: string;
description: string;
remarks: string;
slug: string;
init_answer: any;
//
name: string;
avatar: string;
email: string;
phone: string;
quota: number;
status: 'pending' | 'active' | 'blocked' | 'NA';
createdAt: Date;
}
export interface CreateFormProps {
cat_name: string;
cat_image: File[] | null;
pos: number;
init_answer?: string;
visible: string;
slug: string;
remarks?: string;
description?: string;
//
// TODO: to remove
type: string;
isActive: boolean;
order: number;
name?: string;
imageUrl?: string;
}
export interface EditFormProps {
cat_name: string;
cat_image: File[] | null;
pos: number;
init_answer: any;
visible: string;
slug: string;
remarks?: string;
description?: string;
//
// TODO: remove below
type: string;
}
export interface Helloworld {
helloworld: string;
}

View File

@@ -1 +0,0 @@
please review and add translations, e.g. `{t('[word]')}`

View File

@@ -1,44 +0,0 @@
import { dayjs } from '@/lib/dayjs';
import { CreateFormProps, LpQuestion } from './type';
export const defaultLpQuestion: LpQuestion = {
isEmpty: false,
id: 'default-id',
cat_name: 'default-category-name',
cat_image_url: undefined,
cat_image: undefined,
pos: 0,
visible: 'hidden',
lesson_id: 'default-lesson-id',
description: 'default-description',
remarks: 'default-remarks',
slug: '',
init_answer: {},
// from pocketbase
collectionId: '0000000000',
createdAt: dayjs('2099-01-01').toDate(),
//
name: '',
avatar: '',
email: '',
phone: '',
quota: 0,
status: 'NA',
};
// export const LpCategoryCreateFormDefault: CreateFormProps = {
// name: '',
// type: '',
// pos: 1,
// visible: 'visible',
// description: '',
// isActive: true,
// order: 1,
// imageUrl: '',
// };
export const emptyLpCategory: LpQuestion = {
...defaultLpQuestion,
isEmpty: true,
};

View File

@@ -1,122 +0,0 @@
'use client';
import * as React from 'react';
import deleteQuizLPQuestions from '@/db/QuizLPQuestions/Delete';
import { LoadingButton } from '@mui/lab';
import { Button, Container, Modal, Paper } from '@mui/material';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note';
import { useTranslation } from 'react-i18next';
import { logger } from '@/lib/default-logger';
import { toast } from '@/components/core/toaster';
export default function ConfirmDeleteModal({
open,
setOpen,
idToDelete,
reloadRows,
}: {
open: boolean;
setOpen: (b: boolean) => void;
idToDelete: string;
reloadRows: () => void;
}): React.JSX.Element {
const { t } = useTranslation();
// const handleClose = () => setOpen(false);
function handleClose(): void {
setOpen(false);
}
const [isDeleteing, setIsDeleteing] = React.useState(false);
const style = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
};
function handleUserConfirmDelete(): void {
if (idToDelete) {
setIsDeleteing(true);
deleteQuizLPQuestions(idToDelete)
.then(() => {
reloadRows();
handleClose();
toast(t('delete.success'));
})
.catch((err) => {
// console.error(err)
logger.error(err);
toast(t('delete.error'));
})
.finally(() => {
setIsDeleteing(false);
});
}
}
return (
<div>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<Container maxWidth="sm">
<Paper sx={{ border: '1px solid var(--mui-palette-divider)', boxShadow: 'var(--mui-shadows-16)' }}>
<Stack
direction="row"
spacing={2}
sx={{ display: 'flex', p: 3 }}
>
<Avatar sx={{ bgcolor: 'var(--mui-palette-error-50)', color: 'var(--mui-palette-error-main)' }}>
<NoteIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
<Stack spacing={3}>
<Stack spacing={1}>
<Typography variant="h5">{t('Delete Lesson Type ?')}</Typography>
<Typography
color="text.secondary"
variant="body2"
>
{t('Are you sure you want to delete lesson type ?')}
</Typography>
</Stack>
<Stack
direction="row"
spacing={2}
sx={{ justifyContent: 'flex-end' }}
>
<Button
color="secondary"
onClick={handleClose}
>
{t('Cancel')}
</Button>
<LoadingButton
color="error"
variant="contained"
onClick={(e) => {
handleUserConfirmDelete();
}}
loading={isDeleteing}
>
{t('Delete')}
</LoadingButton>
</Stack>
</Stack>
</Stack>
</Paper>
</Container>
</Box>
</Modal>
</div>
);
}

View File

@@ -1,419 +0,0 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useRouter } from 'next/navigation';
import { COL_QUIZ_LP_QUESTIONS } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
import { Avatar, Divider, MenuItem, Select } from '@mui/material';
// import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2';
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
// import axios from 'axios';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
import { TextEditor } from '@/components/core/text-editor/text-editor';
import { toast } from '@/components/core/toaster';
import type { CreateFormProps } from './type';
const schema = zod.object({
cat_name: zod.string().min(1, 'name-is-required').max(255),
cat_image: zod.array(zod.any()).optional(),
pos: zod.number().min(1, 'position is required').max(99),
init_answer: zod.string().optional(),
visible: zod.string(),
slug: zod.string().min(1, 'slug-is-required').max(255),
remarks: zod.string().optional(),
description: zod.string().optional(),
// NOTE: for image handling
avatar: zod.string().optional(),
// TODO: remove me
type: zod.string().optional(),
isActive: zod.boolean().optional(),
order: zod.number().optional(),
});
type Values = zod.infer<typeof schema>;
export const defaultValues = {
cat_name: '',
cat_image: undefined,
pos: 1,
init_answer: '',
visible: 'hidden',
slug: '',
remarks: '',
description: '',
} satisfies Values;
export function LpQuestionCreateForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_questions']);
const [isCreating, setIsCreating] = React.useState<boolean>(false);
const {
control,
handleSubmit,
formState: { errors },
setValue,
watch,
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
setIsCreating(true);
const payload: CreateFormProps = {
cat_name: values.cat_name,
cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null,
pos: values.pos,
init_answer: values.init_answer,
visible: values.visible,
slug: values.slug,
remarks: values.remarks,
description: values.description,
//
// TODO: remove me
type: 'type tet',
isActive: true,
order: 1,
};
try {
const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).create(payload);
logger.debug(result);
toast.success(t('create.success'));
router.push(paths.dashboard.lp_questions.list);
} catch (error) {
logger.error(error);
toast.error(t('create.failed'));
} finally {
setIsCreating(false);
}
},
// t is not necessary here
// eslint-disable-next-line react-hooks/exhaustive-deps
[router]
);
const avatarInputRef = React.useRef<HTMLInputElement>(null);
const avatar = watch('avatar');
const handleAvatarChange = React.useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const url = await fileToBase64(file);
setValue('avatar', url);
}
},
[setValue]
);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Card>
<CardContent>
<Stack
divider={<Divider />}
spacing={4}
>
<Stack spacing={3}>
<Typography variant="h6">{t('create.basic-info')}</Typography>
<Grid
container
spacing={3}
>
<Grid xs={12}>
<Stack
direction="row"
spacing={3}
sx={{ alignItems: 'center' }}
>
<Box
sx={{
border: '1px dashed var(--mui-palette-divider)',
borderRadius: '5%',
display: 'inline-flex',
p: '4px',
}}
>
<Avatar
variant="rounded"
src={avatar}
sx={{
'--Avatar-size': '100px',
'--Icon-fontSize': 'var(--icon-fontSize-lg)',
alignItems: 'center',
bgcolor: 'var(--mui-palette-background-level1)',
color: 'var(--mui-palette-text-primary)',
display: 'flex',
justifyContent: 'center',
}}
>
<CameraIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
</Box>
<Stack
spacing={1}
sx={{ alignItems: 'flex-start' }}
>
<Typography variant="subtitle1">{t('create.avatar')}</Typography>
<Typography variant="caption">{t('create.avatarRequirements')}</Typography>
<Button
color="secondary"
onClick={() => {
avatarInputRef.current?.click();
}}
variant="outlined"
>
{t('create.avatar_select')}
</Button>
<input
hidden
onChange={handleAvatarChange}
ref={avatarInputRef}
type="file"
/>
</Stack>
</Stack>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="cat_name"
render={({ field }) => (
<FormControl
disabled={isCreating}
error={Boolean(errors.cat_name)}
fullWidth
>
<InputLabel required>{t('create.cat_name')}</InputLabel>
<OutlinedInput {...field} />
{errors.cat_name ? <FormHelperText>{errors.cat_name.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="pos"
render={({ field }) => (
<FormControl
error={Boolean(errors.pos)}
fullWidth
>
<InputLabel required>{t('create.pos')}</InputLabel>
<OutlinedInput
{...field}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
type="number"
/>
{errors.pos ? <FormHelperText>{errors.pos.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="slug"
render={({ field }) => (
<FormControl
error={Boolean(errors.slug)}
fullWidth
>
<InputLabel required>{t('create.slug')}</InputLabel>
<OutlinedInput {...field} />
{errors.slug ? <FormHelperText>{errors.slug.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="visible"
render={({ field }) => (
<FormControl
error={Boolean(errors.visible)}
fullWidth
>
<InputLabel>{t('edit.visible')}</InputLabel>
<Select {...field}>
<MenuItem value="visible">{t('edit.visible')}</MenuItem>
<MenuItem value="hidden">{t('edit.hidden')}</MenuItem>
</Select>
{errors.visible ? <FormHelperText>{errors.visible.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="init_answer"
render={({ field }) => (
<FormControl
error={Boolean(errors.init_answer)}
fullWidth
>
<InputLabel required>{t('create.init_answer')}</InputLabel>
<OutlinedInput {...field} />
{errors.init_answer ? <FormHelperText>{errors.init_answer.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
{/* */}
<Stack spacing={3}>
<Typography variant="h6">{t('create.detail-information')}</Typography>
<Grid
container
spacing={3}
>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="description"
render={({ field }) => {
return (
<Box>
<Typography
variant="subtitle1"
color="text-secondary"
>
{t('create.description')}
</Typography>
<Box sx={{ mt: '8px', '& .tiptap-container': { height: '400px' } }}>
<TextEditor
{...field}
content=""
onUpdate={({ editor }) => {
field.onChange({ target: { value: editor.getHTML() } });
}}
placeholder={t('create.description.default')}
/>
</Box>
</Box>
);
}}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="remarks"
render={({ field }) => (
<Box>
<Typography
variant="subtitle1"
color="text.secondary"
>
{t('create.remarks')}
</Typography>
<Box sx={{ mt: '8px', '& .tiptap-container': { height: '400px' } }}>
<TextEditor
content=""
onUpdate={({ editor }) => {
field.onChange({ target: { value: editor.getText() } });
}}
hideToolbar
placeholder={t('create.remarks.default')}
/>
</Box>
</Box>
)}
/>
</Grid>
</Grid>
</Stack>
{/* */}
</Stack>
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}>
<Button
color="secondary"
component={RouterLink}
href={paths.dashboard.lp_questions.list}
>
{t('create.cancelButton')}
</Button>
<LoadingButton
disabled={isCreating}
loading={isCreating}
type="submit"
variant="contained"
>
{t('create.createButton')}
</LoadingButton>
</CardActions>
</Card>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify({ errors }, null, 2)}</pre>
</Box>
</form>
);
}

View File

@@ -1,500 +0,0 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
//
import { COL_QUIZ_LP_QUESTIONS } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
import Divider from '@mui/material/Divider';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2';
//
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
//
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
import { TextEditor } from '@/components/core/text-editor/text-editor';
import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading';
import ErrorDisplay from '../error';
import type { EditFormProps } from './type';
// TODO: review this
const schema = zod.object({
cat_name: zod.string().min(1, 'name-is-required').max(255),
// accept file object when user change image
// accept http string when user not changing image
cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(),
// position
pos: zod.number().min(1, 'position is required').max(99),
// it should be a valid JSON
init_answer: zod
.string()
.refine(
(value) => {
try {
JSON.parse(value);
return true;
} catch (error) {
return false;
}
},
{ message: 'init_answer must be a valid JSON' }
)
.optional(),
visible: zod.string(),
slug: zod.string().min(0, 'slug-is-required').max(255).optional(),
remarks: zod.string().optional(),
description: zod.string().optional(),
// NOTE: for image handling
avatar: zod.string().optional(),
});
type Values = zod.infer<typeof schema>;
const defaultValues = {
cat_name: '',
cat_image: undefined,
pos: 1,
init_answer: JSON.stringify({}),
visible: 'hidden',
slug: '',
remarks: '',
description: '',
} satisfies Values;
export function LpQuestionEditForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_questions']);
const { cat_id: catId } = useParams<{ cat_id: string }>();
//
const [isUpdating, setIsUpdating] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(false);
//
const [showError, setShowError] = React.useState({ show: false, detail: '' });
const {
control,
handleSubmit,
formState: { errors },
setValue,
reset,
watch,
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
setIsUpdating(true);
const tempUpdate: EditFormProps = {
cat_name: values.cat_name,
cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null,
pos: values.pos,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
init_answer: JSON.parse(values.init_answer || '{}'),
visible: values.visible,
slug: values.slug || 'not-defined',
remarks: values.remarks,
description: values.description,
//
// TODO: remove below
type: '',
};
try {
const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).update(catId, tempUpdate);
logger.debug(result);
toast.success(t('edit.success'));
router.push(paths.dashboard.lp_questions.list);
} catch (error) {
logger.error(error);
toast.error(t('update.failed'));
} finally {
setIsUpdating(false);
}
},
// t is not necessary here
// eslint-disable-next-line react-hooks/exhaustive-deps
[router]
);
const avatarInputRef = React.useRef<HTMLInputElement>(null);
const avatar = watch('avatar');
const handleAvatarChange = React.useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const url = await fileToBase64(file);
setValue('avatar', url);
}
},
[setValue]
);
// TODO: need to align with save form
// use trycatch
const [textDescription, setTextDescription] = React.useState<string>('');
const [textRemarks, setTextRemarks] = React.useState<string>('');
// load existing data when user arrive
const loadExistingData = React.useCallback(
async (id: string) => {
setShowLoading(true);
try {
const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).getOne(id);
reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) });
setTextDescription(result.description);
setTextRemarks(result.remarks);
if (result.cat_image !== '') {
const fetchResult = await fetch(
`http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}`
);
const blob = await fetchResult.blob();
const url = await fileToBase64(blob);
setValue('avatar', url);
} else {
setValue('avatar', '');
}
} catch (error) {
logger.error(error);
toast(t('list.error'));
setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
} finally {
setShowLoading(false);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[catId]
);
React.useEffect(() => {
void loadExistingData(catId);
// 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 (
<form onSubmit={handleSubmit(onSubmit)}>
<Card>
<CardContent>
<Stack
divider={<Divider />}
spacing={4}
>
<Stack spacing={3}>
<Typography variant="h6">{t('edit.basic-info')}</Typography>
<Grid
container
spacing={3}
>
<Grid xs={12}>
<Stack
direction="row"
spacing={3}
sx={{ alignItems: 'center' }}
>
<Box
sx={{
border: '1px dashed var(--mui-palette-divider)',
borderRadius: '5%',
display: 'inline-flex',
p: '4px',
}}
>
<Avatar
variant="rounded"
src={avatar}
sx={{
'--Avatar-size': '100px',
'--Icon-fontSize': 'var(--icon-fontSize-lg)',
alignItems: 'center',
bgcolor: 'var(--mui-palette-background-level1)',
color: 'var(--mui-palette-text-primary)',
display: 'flex',
justifyContent: 'center',
}}
>
<CameraIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
</Box>
<Stack
spacing={1}
sx={{ alignItems: 'flex-start' }}
>
<Typography variant="subtitle1">{t('edit.avatar')}</Typography>
<Typography variant="caption">{t('edit.avatarRequirements')}</Typography>
<Button
color="secondary"
onClick={() => {
avatarInputRef.current?.click();
}}
variant="outlined"
>
{t('edit.avatar_select')}
</Button>
<input
hidden
onChange={handleAvatarChange}
ref={avatarInputRef}
type="file"
/>
</Stack>
</Stack>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="cat_name"
render={({ field }) => (
<FormControl
disabled={isUpdating}
error={Boolean(errors.cat_name)}
fullWidth
>
<InputLabel required>{t('edit.cat_name')}</InputLabel>
<OutlinedInput {...field} />
{errors.cat_name ? <FormHelperText>{errors.cat_name.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="pos"
render={({ field }) => (
<FormControl
error={Boolean(errors.pos)}
fullWidth
>
<InputLabel required>{t('edit.pos')}</InputLabel>
<OutlinedInput
{...field}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
type="number"
/>
{errors.pos ? <FormHelperText>{errors.pos.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="slug"
render={({ field }) => (
<FormControl
error={Boolean(errors.slug)}
fullWidth
>
<InputLabel required>{t('edit.slug')}</InputLabel>
<OutlinedInput {...field} />
{errors.slug ? <FormHelperText>{errors.slug.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="visible"
render={({ field }) => (
<FormControl
error={Boolean(errors.visible)}
fullWidth
>
<InputLabel>{t('edit.visible')}</InputLabel>
<Select {...field}>
<MenuItem value="visible">{t('edit.visible')}</MenuItem>
<MenuItem value="hidden">{t('edit.hidden')}</MenuItem>
</Select>
{errors.visible ? <FormHelperText>{errors.visible.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="init_answer"
render={({ field }) => (
<FormControl
error={Boolean(errors.init_answer)}
fullWidth
>
<InputLabel required>{t('edit.init_answer')}</InputLabel>
<OutlinedInput {...field} />
{errors.init_answer ? <FormHelperText>{errors.init_answer.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
{/* */}
<Stack spacing={3}>
<Typography variant="h6">{t('edit.detail-information')}</Typography>
<Grid
container
spacing={3}
>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="description"
render={({ field }) => {
return (
<Box>
<Typography
variant="subtitle1"
color="text-secondary"
>
{t('edit.description')}
</Typography>
<Box sx={{ mt: '8px', '& .tiptap-container': { height: '200px' } }}>
<TextEditor
{...field}
content={textDescription}
onUpdate={({ editor }) => {
field.onChange({ target: { value: editor.getHTML() } });
}}
placeholder={t('edit.description.default')}
/>
</Box>
</Box>
);
}}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isUpdating}
control={control}
name="remarks"
render={({ field }) => (
<Box>
<Typography
variant="subtitle1"
color="text.secondary"
>
{t('edit.remarks')}
</Typography>
<Box sx={{ mt: '8px', '& .tiptap-container': { height: '200px' } }}>
<TextEditor
content={textRemarks}
onUpdate={({ editor }) => {
field.onChange({ target: { value: editor.getText() } });
}}
hideToolbar
placeholder={t('edit.remarks.default')}
/>
</Box>
</Box>
)}
/>
</Grid>
</Grid>
</Stack>
{/* */}
</Stack>
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}>
<Button
color="secondary"
component={RouterLink}
href={paths.dashboard.lp_questions.list}
>
{t('edit.cancelButton')}
</Button>
<LoadingButton
disabled={isUpdating}
loading={isUpdating}
type="submit"
variant="contained"
>
{t('edit.updateButton')}
</LoadingButton>
</CardActions>
</Card>
</form>
);
}

View File

@@ -1,456 +0,0 @@
'use client';
import * as React from 'react';
import { useRouter } from 'next/navigation';
// import { COL_LESSON_CATEGORIES } from '@/constants';
import GetAllCount from '@/db/QuizListenings/GetAllCount';
import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount';
import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
import FormControl from '@mui/material/FormControl';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import type { SelectChangeEvent } from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
import { Option } from '@/components/core/option';
import { useLpCategoriesSelection } from './lp-questions-selection-context';
import { LpQuestion } from './type';
export interface Filters {
email?: string;
phone?: string;
status?: string;
name?: string;
visible?: string;
type?: string;
}
export type SortDir = 'asc' | 'desc';
export interface LpCategoriesFiltersProps {
filters?: Filters;
sortDir?: SortDir;
fullData: LpQuestion[];
}
export function LpQuestionsFilters({
filters = {},
sortDir = 'desc',
fullData,
}: LpCategoriesFiltersProps): React.JSX.Element {
const { t } = useTranslation();
const { email, phone, status, name, visible, type } = filters;
const [totalCount, setTotalCount] = React.useState<number>(0);
const [visibleCount, setVisibleCount] = React.useState<number>(0);
const [hiddenCount, setHiddenCount] = React.useState<number>(0);
const router = useRouter();
const selection = useLpCategoriesSelection();
function getVisible(): number {
return fullData.reduce((count, item: LpQuestion) => {
return item.visible === 'visible' ? count + 1 : count;
}, 0);
}
function getHidden(): number {
return fullData.reduce((count, item: LpQuestion) => {
return item.visible === 'hidden' ? count + 1 : count;
}, 0);
}
// The tabs should be generated using API data.
const tabs = [
{ label: t('All'), value: '', count: totalCount },
// { label: 'Active', value: 'active', count: 3 },
// { label: 'Pending', value: 'pending', count: 1 },
// { label: 'Blocked', value: 'blocked', count: 1 },
{ label: t('visible'), value: 'visible', count: visibleCount },
{ label: t('hidden'), value: 'hidden', count: hiddenCount },
] as const;
const updateSearchParams = React.useCallback(
(newFilters: Filters, newSortDir: SortDir): void => {
const searchParams = new URLSearchParams();
if (newSortDir === 'asc') {
searchParams.set('sortDir', newSortDir);
}
if (newFilters.status) {
searchParams.set('status', newFilters.status);
}
if (newFilters.email) {
searchParams.set('email', newFilters.email);
}
if (newFilters.phone) {
searchParams.set('phone', newFilters.phone);
}
if (newFilters.name) {
searchParams.set('name', newFilters.name);
}
if (newFilters.type) {
searchParams.set('type', newFilters.type);
}
if (newFilters.visible) {
searchParams.set('visible', newFilters.visible);
}
// NOTE: modify according to COLLECTION
router.push(`${paths.dashboard.lp_questions.list}?${searchParams.toString()}`);
},
[router]
);
const handleClearFilters = React.useCallback(() => {
updateSearchParams({}, sortDir);
}, [updateSearchParams, sortDir]);
const handleStatusChange = React.useCallback(
(_: React.SyntheticEvent, value: string) => {
updateSearchParams({ ...filters, status: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleVisibleChange = React.useCallback(
(_: React.SyntheticEvent, value: string) => {
updateSearchParams({ ...filters, visible: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleNameChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, name: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleTypeChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, type: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleEmailChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, email: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handlePhoneChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, phone: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleSortChange = React.useCallback(
(event: SelectChangeEvent) => {
updateSearchParams(filters, event.target.value as SortDir);
},
[updateSearchParams, filters]
);
React.useEffect(() => {
const fetchCount = async (): Promise<void> => {
try {
const tc = await GetAllCount();
setTotalCount(tc);
const vc = await GetVisibleCount();
setVisibleCount(vc);
const hc = await GetHiddenCount();
setHiddenCount(hc);
} catch (error) {
//
}
};
void fetchCount();
}, []);
const hasFilters = status || email || phone || visible || name || type;
return (
<div>
<Tabs
onChange={handleVisibleChange}
sx={{ px: 3 }}
value={visible ?? ''}
variant="scrollable"
>
{tabs.map((tab) => (
<Tab
icon={
<Chip
label={tab.count}
size="small"
variant="soft"
/>
}
iconPosition="end"
key={tab.value}
label={tab.label}
sx={{ minHeight: 'auto' }}
tabIndex={0}
value={tab.value}
/>
))}
</Tabs>
<Divider />
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flexWrap: 'wrap', px: 3, py: 2 }}
>
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flex: '1 1 auto', flexWrap: 'wrap' }}
>
<FilterButton
displayValue={name}
label={t('Name')}
onFilterApply={(value) => {
handleNameChange(value as string);
}}
onFilterDelete={() => {
handleNameChange();
}}
popover={<NameFilterPopover />}
value={name}
/>
<FilterButton
displayValue={type}
label={t('Type')}
onFilterApply={(value) => {
handleTypeChange(value as string);
}}
onFilterDelete={() => {
handleTypeChange();
}}
popover={<TypeFilterPopover />}
value={type}
/>
{hasFilters ? <Button onClick={handleClearFilters}>{t('Clear filters')}</Button> : null}
</Stack>
{selection.selectedAny ? (
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center' }}
>
<Typography
color="text.secondary"
variant="body2"
>
{selection.selected.size} {t('selected')}
</Typography>
<Button
color="error"
variant="contained"
>
{t('Delete')}
</Button>
</Stack>
) : null}
<Select
name="sort"
onChange={handleSortChange}
sx={{ maxWidth: '100%', width: '120px' }}
value={sortDir}
>
<Option value="desc">{t('Newest')}</Option>
<Option value="asc">{t('Oldest')}</Option>
</Select>
</Stack>
</div>
);
}
function TypeFilterPopover(): React.JSX.Element {
const { t } = useTranslation();
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title={t('Filter by type')}
>
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
{t('Apply')}
</Button>
</FilterPopover>
);
}
function NameFilterPopover(): React.JSX.Element {
const { t } = useTranslation();
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title={t('Filter by name')}
>
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
{t('apply')}
</Button>
</FilterPopover>
);
}
function EmailFilterPopover(): React.JSX.Element {
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
const { t } = useTranslation();
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title="Filter by email"
>
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
{t('Apply')}
</Button>
</FilterPopover>
);
}
function PhoneFilterPopover(): React.JSX.Element {
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
const { t } = useTranslation();
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title="Filter by phone number"
>
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
{t('Apply')}
</Button>
</FilterPopover>
);
}

View File

@@ -1,46 +0,0 @@
'use client';
import * as React from 'react';
// import type { LessonCategory } from '@/types/lesson-type';
import { useSelection } from '@/hooks/use-selection';
import type { Selection } from '@/hooks/use-selection';
import { LpQuestion } from './type';
function noop(): void {
return undefined;
}
export interface LpCategoriesSelectionContextValue extends Selection {}
export const LpCategoriesSelectionContext = React.createContext<LpCategoriesSelectionContextValue>({
deselectAll: noop,
deselectOne: noop,
selectAll: noop,
selectOne: noop,
selected: new Set(),
selectedAny: false,
selectedAll: false,
});
interface LpCategoriesSelectionProviderProps {
children: React.ReactNode;
lessonQuestions: LpQuestion[];
}
export function LpQuestionsSelectionProvider({
children,
lessonQuestions: lessonCategories = [],
}: LpCategoriesSelectionProviderProps): React.JSX.Element {
const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]);
const selection = useSelection(customerIds);
return (
<LpCategoriesSelectionContext.Provider value={{ ...selection }}>{children}</LpCategoriesSelectionContext.Provider>
);
}
export function useLpCategoriesSelection(): LpCategoriesSelectionContextValue {
return React.useContext(LpCategoriesSelectionContext);
}

View File

@@ -1,241 +0,0 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { LoadingButton } from '@mui/lab';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import LinearProgress from '@mui/material/LinearProgress';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
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 { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus';
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { paths } from '@/paths';
import { dayjs } from '@/lib/dayjs';
import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table';
import ConfirmDeleteModal from './confirm-delete-modal';
import { useLpCategoriesSelection } from './lp-questions-selection-context';
import type { LpQuestion } from './type';
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpQuestion>[] {
return [
{
formatter: (row): React.JSX.Element => (
<Stack
direction="row"
spacing={1}
sx={{ alignItems: 'center' }}
>
<Link
color="inherit"
component={RouterLink}
href={paths.dashboard.lp_questions.details(row.id)}
sx={{ whiteSpace: 'nowrap' }}
variant="subtitle2"
>
<Stack
direction="row"
spacing={1}
sx={{ alignItems: 'center' }}
>
<Avatar
src={`http://127.0.0.1:8090/api/files/${row.collectionId}/${row.id}/${row.cat_image}`}
variant="rounded"
>
<ImagesIcon size={32} />
</Avatar>{' '}
<div>
<Box sx={{ whiteSpace: 'nowrap' }}>{row.cat_name}</Box>
<Typography
color="text.secondary"
variant="body2"
>
slug: {row.cat_name}
</Typography>
</div>
</Stack>
</Link>
</Stack>
),
name: 'Name',
width: '200px',
},
{
formatter: (row): React.JSX.Element => (
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center' }}
>
<LinearProgress
sx={{ flex: '1 1 auto' }}
value={row.quota}
variant="determinate"
/>
<Typography
color="text.secondary"
variant="body2"
>
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)}
</Typography>
</Stack>
),
// NOTE: please refer to translation.json here
name: 'word-count',
width: '100px',
},
{
formatter: (row): React.JSX.Element => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const mapping = {
active: {
label: 'Active',
icon: (
<CheckCircleIcon
color="var(--mui-palette-success-main)"
weight="fill"
/>
),
},
blocked: { label: 'Blocked', icon: <MinusIcon color="var(--mui-palette-error-main)" /> },
pending: {
label: 'Pending',
icon: (
<ClockIcon
color="var(--mui-palette-warning-main)"
weight="fill"
/>
),
},
NA: {
label: 'NA',
icon: (
<ClockIcon
color="var(--mui-palette-warning-main)"
weight="fill"
/>
),
},
} as const;
const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
return (
<Button
onClick={() => {
toast.error('sorry but not implementd');
}}
style={{ backgroundColor: 'transparent' }}
>
{/* <Chip icon={icon} label={label} size="small" variant="outlined" /> */}
{label}
</Button>
);
},
name: 'visible',
width: '150px',
},
{
formatter(row) {
return dayjs(row.createdAt).format('MMM D, YYYY');
},
name: 'Created at',
width: '100px',
},
{
formatter: (row): React.JSX.Element => (
<Stack
direction="row"
spacing={1}
>
<LoadingButton
//
color="secondary"
component={RouterLink}
href={paths.dashboard.lp_questions.details(row.id)}
>
<PencilSimpleIcon size={24} />
</LoadingButton>
<LoadingButton
color="error"
disabled={row.isEmpty}
onClick={() => {
handleDeleteClick(row.id);
}}
>
<TrashSimpleIcon size={24} />
</LoadingButton>
</Stack>
),
name: 'Actions',
hideName: true,
width: '100px',
align: 'right',
},
];
}
export interface LessonCategoriesTableProps {
rows: LpQuestion[];
reloadRows: () => void;
}
export function LpQuestionsTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element {
const { t } = useTranslation(['lp_questions']);
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpCategoriesSelection();
const [idToDelete, setIdToDelete] = React.useState('');
const [open, setOpen] = React.useState(false);
function handleDeleteClick(testId: string): void {
setOpen(true);
setIdToDelete(testId);
}
return (
<React.Fragment>
<ConfirmDeleteModal
idToDelete={idToDelete}
open={open}
reloadRows={reloadRows}
setOpen={setOpen}
/>
<DataTable<LpQuestion>
columns={columns(handleDeleteClick)}
onDeselectAll={deselectAll}
onDeselectOne={(_, row) => {
deselectOne(row.id);
}}
onSelectAll={selectAll}
onSelectOne={(_, row) => {
selectOne(row.id);
}}
rows={rows}
selectable
selected={selected}
/>
{!rows.length ? (
<Box sx={{ p: 3 }}>
<Typography
color="text.secondary"
sx={{ textAlign: 'center' }}
variant="body2"
>
{t('no-lesson-categories-found')}
</Typography>
</Box>
) : null}
</React.Fragment>
);
}

View File

@@ -1,101 +0,0 @@
'use client';
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Chip from '@mui/material/Chip';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple';
import { dayjs } from '@/lib/dayjs';
import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table';
import { Option } from '@/components/core/option';
export interface Notification {
id: string;
type: string;
status: 'delivered' | 'pending' | 'failed';
createdAt: Date;
}
const columns = [
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="inherit">
{row.type}
</Typography>
),
name: 'Type',
width: '300px',
},
{
formatter: (row): React.JSX.Element => {
const mapping = {
delivered: { label: 'Delivered', color: 'success' },
pending: { label: 'Pending', color: 'warning' },
failed: { label: 'Failed', color: 'error' },
} as const;
const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' };
return <Chip color={color} label={label} size="small" variant="soft" />;
},
name: 'Status',
width: '200px',
},
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="inherit">
{dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')}
</Typography>
),
name: 'Date',
align: 'right',
},
] satisfies ColumnDef<Notification>[];
export interface NotificationsProps {
notifications: Notification[];
}
export function Notifications({ notifications }: NotificationsProps): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<EnvelopeSimpleIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Notifications"
/>
<CardContent>
<Stack spacing={3}>
<Stack spacing={2}>
<Select defaultValue="last_invoice" name="type" sx={{ maxWidth: '100%', width: '320px' }}>
<Option value="last_invoice">Resend last invoice</Option>
<Option value="password_reset">Send password reset</Option>
<Option value="verification">Send verification</Option>
</Select>
<div>
<Button startIcon={<EnvelopeSimpleIcon />} variant="contained">
Send email
</Button>
</div>
</Stack>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<Box sx={{ overflowX: 'auto' }}>
<DataTable<Notification> columns={columns} rows={notifications} />
</Box>
</Card>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -1,138 +0,0 @@
'use client';
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple';
import { dayjs } from '@/lib/dayjs';
import type { ColumnDef } from '@/components/core/data-table';
import { DataTable } from '@/components/core/data-table';
export interface Payment {
currency: string;
amount: number;
invoiceId: string;
status: 'pending' | 'completed' | 'canceled' | 'refunded';
createdAt: Date;
}
const columns = [
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="subtitle2">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)}
</Typography>
),
name: 'Amount',
width: '200px',
},
{
formatter: (row): React.JSX.Element => {
const mapping = {
pending: { label: 'Pending', color: 'warning' },
completed: { label: 'Completed', color: 'success' },
canceled: { label: 'Canceled', color: 'error' },
refunded: { label: 'Refunded', color: 'error' },
} as const;
const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' };
return <Chip color={color} label={label} size="small" variant="soft" />;
},
name: 'Status',
width: '200px',
},
{
formatter: (row): React.JSX.Element => {
return <Link variant="inherit">{row.invoiceId}</Link>;
},
name: 'Invoice ID',
width: '150px',
},
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="inherit">
{dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')}
</Typography>
),
name: 'Date',
align: 'right',
},
] satisfies ColumnDef<Payment>[];
export interface PaymentsProps {
ordersValue: number;
payments: Payment[];
refundsValue: number;
totalOrders: number;
}
export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element {
return (
<Card>
<CardHeader
action={
<Button color="secondary" startIcon={<PlusIcon />}>
Create Payment
</Button>
}
avatar={
<Avatar>
<ShoppingCartSimpleIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Payments"
/>
<CardContent>
<Stack spacing={3}>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<Stack
direction="row"
divider={<Divider flexItem orientation="vertical" />}
spacing={3}
sx={{ justifyContent: 'space-between', p: 2 }}
>
<div>
<Typography color="text.secondary" variant="overline">
Total orders
</Typography>
<Typography variant="h6">{new Intl.NumberFormat('en-US').format(totalOrders)}</Typography>
</div>
<div>
<Typography color="text.secondary" variant="overline">
Orders value
</Typography>
<Typography variant="h6">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)}
</Typography>
</div>
<div>
<Typography color="text.secondary" variant="overline">
Refunds
</Typography>
<Typography variant="h6">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)}
</Typography>
</div>
</Stack>
</Card>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<Box sx={{ overflowX: 'auto' }}>
<DataTable<Payment> columns={columns} rows={payments} />
</Box>
</Card>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -1,46 +0,0 @@
import * as React from 'react';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Chip from '@mui/material/Chip';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
export interface Address {
id: string;
country: string;
state: string;
city: string;
zipCode: string;
street: string;
primary?: boolean;
}
export interface ShippingAddressProps {
address: Address;
}
export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement {
return (
<Card sx={{ borderRadius: 1, height: '100%' }} variant="outlined">
<CardContent>
<Stack spacing={2}>
<Typography>
{address.street},
<br />
{address.city}, {address.state}, {address.country},
<br />
{address.zipCode}
</Typography>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'space-between' }}>
{address.primary ? <Chip color="warning" label="Primary" variant="soft" /> : <span />}
<Button color="secondary" size="small" startIcon={<PencilSimpleIcon />}>
Edit
</Button>
</Stack>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -1,61 +0,0 @@
export interface LpQuestion {
isEmpty?: boolean;
//
id: string;
collectionId: string;
//
cat_name: string;
cat_image_url?: string;
cat_image?: string;
pos: number;
visible: string;
lesson_id: string;
description: string;
remarks: string;
slug: string;
init_answer: any;
//
name: string;
avatar: string;
email: string;
phone: string;
quota: number;
status: 'pending' | 'active' | 'blocked' | 'NA';
createdAt: Date;
}
export interface CreateFormProps {
cat_name: string;
cat_image: File[] | null;
pos: number;
init_answer?: string;
visible: string;
slug: string;
remarks?: string;
description?: string;
//
// TODO: to remove
type: string;
isActive: boolean;
order: number;
name?: string;
imageUrl?: string;
}
export interface EditFormProps {
cat_name: string;
cat_image: File[] | null;
pos: number;
init_answer: any;
visible: string;
slug: string;
remarks?: string;
description?: string;
//
// TODO: remove below
type: string;
}
export interface Helloworld {
helloworld: string;
}

View File

@@ -1,8 +1,8 @@
import { dayjs } from '@/lib/dayjs';
import { CreateFormProps, LpCategory } from './type';
import { CreateFormProps, MfCategory } from './type';
export const defaultLpCategory: LpCategory = {
export const defaultMfCategory: MfCategory = {
isEmpty: false,
id: 'default-id',
cat_name: 'default-category-name',
@@ -38,7 +38,7 @@ export const defaultLpCategory: LpCategory = {
// imageUrl: '',
// };
export const emptyLpCategory: LpCategory = {
...defaultLpCategory,
export const emptyLpCategory: MfCategory = {
...defaultMfCategory,
isEmpty: true,
};

View File

@@ -1,456 +0,0 @@
'use client';
import * as React from 'react';
import { useRouter } from 'next/navigation';
// import { COL_LESSON_CATEGORIES } from '@/constants';
import GetAllCount from '@/db/QuizListenings/GetAllCount';
import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount';
import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
import FormControl from '@mui/material/FormControl';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import type { SelectChangeEvent } from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
import { Option } from '@/components/core/option';
import { useLpCategoriesSelection } from './lp-categories-selection-context';
import { LpCategory } from './type';
export interface Filters {
email?: string;
phone?: string;
status?: string;
name?: string;
visible?: string;
type?: string;
}
export type SortDir = 'asc' | 'desc';
export interface LpCategoriesFiltersProps {
filters?: Filters;
sortDir?: SortDir;
fullData: LpCategory[];
}
export function LpCategoriesFilters({
filters = {},
sortDir = 'desc',
fullData,
}: LpCategoriesFiltersProps): React.JSX.Element {
const { t } = useTranslation();
const { email, phone, status, name, visible, type } = filters;
const [totalCount, setTotalCount] = React.useState<number>(0);
const [visibleCount, setVisibleCount] = React.useState<number>(0);
const [hiddenCount, setHiddenCount] = React.useState<number>(0);
const router = useRouter();
const selection = useLpCategoriesSelection();
function getVisible(): number {
return fullData.reduce((count, item: LpCategory) => {
return item.visible === 'visible' ? count + 1 : count;
}, 0);
}
function getHidden(): number {
return fullData.reduce((count, item: LpCategory) => {
return item.visible === 'hidden' ? count + 1 : count;
}, 0);
}
// The tabs should be generated using API data.
const tabs = [
{ label: t('All'), value: '', count: totalCount },
// { label: 'Active', value: 'active', count: 3 },
// { label: 'Pending', value: 'pending', count: 1 },
// { label: 'Blocked', value: 'blocked', count: 1 },
{ label: t('visible'), value: 'visible', count: visibleCount },
{ label: t('hidden'), value: 'hidden', count: hiddenCount },
] as const;
const updateSearchParams = React.useCallback(
(newFilters: Filters, newSortDir: SortDir): void => {
const searchParams = new URLSearchParams();
if (newSortDir === 'asc') {
searchParams.set('sortDir', newSortDir);
}
if (newFilters.status) {
searchParams.set('status', newFilters.status);
}
if (newFilters.email) {
searchParams.set('email', newFilters.email);
}
if (newFilters.phone) {
searchParams.set('phone', newFilters.phone);
}
if (newFilters.name) {
searchParams.set('name', newFilters.name);
}
if (newFilters.type) {
searchParams.set('type', newFilters.type);
}
if (newFilters.visible) {
searchParams.set('visible', newFilters.visible);
}
// NOTE: modify according to COLLECTION
router.push(`${paths.dashboard.lp_categories.list}?${searchParams.toString()}`);
},
[router]
);
const handleClearFilters = React.useCallback(() => {
updateSearchParams({}, sortDir);
}, [updateSearchParams, sortDir]);
const handleStatusChange = React.useCallback(
(_: React.SyntheticEvent, value: string) => {
updateSearchParams({ ...filters, status: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleVisibleChange = React.useCallback(
(_: React.SyntheticEvent, value: string) => {
updateSearchParams({ ...filters, visible: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleNameChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, name: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleTypeChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, type: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleEmailChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, email: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handlePhoneChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, phone: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleSortChange = React.useCallback(
(event: SelectChangeEvent) => {
updateSearchParams(filters, event.target.value as SortDir);
},
[updateSearchParams, filters]
);
React.useEffect(() => {
const fetchCount = async (): Promise<void> => {
try {
const tc = await GetAllCount();
setTotalCount(tc);
const vc = await GetVisibleCount();
setVisibleCount(vc);
const hc = await GetHiddenCount();
setHiddenCount(hc);
} catch (error) {
//
}
};
void fetchCount();
}, []);
const hasFilters = status || email || phone || visible || name || type;
return (
<div>
<Tabs
onChange={handleVisibleChange}
sx={{ px: 3 }}
value={visible ?? ''}
variant="scrollable"
>
{tabs.map((tab) => (
<Tab
icon={
<Chip
label={tab.count}
size="small"
variant="soft"
/>
}
iconPosition="end"
key={tab.value}
label={tab.label}
sx={{ minHeight: 'auto' }}
tabIndex={0}
value={tab.value}
/>
))}
</Tabs>
<Divider />
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flexWrap: 'wrap', px: 3, py: 2 }}
>
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flex: '1 1 auto', flexWrap: 'wrap' }}
>
<FilterButton
displayValue={name}
label={t('Name')}
onFilterApply={(value) => {
handleNameChange(value as string);
}}
onFilterDelete={() => {
handleNameChange();
}}
popover={<NameFilterPopover />}
value={name}
/>
<FilterButton
displayValue={type}
label={t('Type')}
onFilterApply={(value) => {
handleTypeChange(value as string);
}}
onFilterDelete={() => {
handleTypeChange();
}}
popover={<TypeFilterPopover />}
value={type}
/>
{hasFilters ? <Button onClick={handleClearFilters}>{t('Clear filters')}</Button> : null}
</Stack>
{selection.selectedAny ? (
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center' }}
>
<Typography
color="text.secondary"
variant="body2"
>
{selection.selected.size} {t('selected')}
</Typography>
<Button
color="error"
variant="contained"
>
{t('Delete')}
</Button>
</Stack>
) : null}
<Select
name="sort"
onChange={handleSortChange}
sx={{ maxWidth: '100%', width: '120px' }}
value={sortDir}
>
<Option value="desc">{t('Newest')}</Option>
<Option value="asc">{t('Oldest')}</Option>
</Select>
</Stack>
</div>
);
}
function TypeFilterPopover(): React.JSX.Element {
const { t } = useTranslation();
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title={t('Filter by type')}
>
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
{t('Apply')}
</Button>
</FilterPopover>
);
}
function NameFilterPopover(): React.JSX.Element {
const { t } = useTranslation();
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title={t('Filter by name')}
>
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
{t('apply')}
</Button>
</FilterPopover>
);
}
function EmailFilterPopover(): React.JSX.Element {
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
const { t } = useTranslation();
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title="Filter by email"
>
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
{t('Apply')}
</Button>
</FilterPopover>
);
}
function PhoneFilterPopover(): React.JSX.Element {
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
const { t } = useTranslation();
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title="Filter by phone number"
>
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
{t('Apply')}
</Button>
</FilterPopover>
);
}

View File

@@ -1,49 +0,0 @@
'use client';
import * as React from 'react';
import TablePagination from '@mui/material/TablePagination';
function noop(): void {
return undefined;
}
interface LessonCategoriesPaginationProps {
count: number;
page: number;
//
setPage: (page: number) => void;
setRowsPerPage: (page: number) => void;
rowsPerPage: number;
}
export function LpCategoriesPagination({
count,
page,
//
setPage,
setRowsPerPage,
rowsPerPage,
}: LessonCategoriesPaginationProps): React.JSX.Element {
// You should implement the pagination using a similar logic as the filters.
// Note that when page change, you should keep the filter search params.
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value));
// console.log(parseInt(event.target.value));
};
return (
<TablePagination
component="div"
count={count}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
page={page}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[5, 10, 25]}
/>
);
}

View File

@@ -1,46 +0,0 @@
'use client';
import * as React from 'react';
// import type { LessonCategory } from '@/types/lesson-type';
import { useSelection } from '@/hooks/use-selection';
import type { Selection } from '@/hooks/use-selection';
import { LpCategory } from './type';
function noop(): void {
return undefined;
}
export interface LpCategoriesSelectionContextValue extends Selection {}
export const LpCategoriesSelectionContext = React.createContext<LpCategoriesSelectionContextValue>({
deselectAll: noop,
deselectOne: noop,
selectAll: noop,
selectOne: noop,
selected: new Set(),
selectedAny: false,
selectedAll: false,
});
interface LpCategoriesSelectionProviderProps {
children: React.ReactNode;
lessonCategories: LpCategory[];
}
export function LpCategoriesSelectionProvider({
children,
lessonCategories = [],
}: LpCategoriesSelectionProviderProps): React.JSX.Element {
const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]);
const selection = useSelection(customerIds);
return (
<LpCategoriesSelectionContext.Provider value={{ ...selection }}>{children}</LpCategoriesSelectionContext.Provider>
);
}
export function useLpCategoriesSelection(): LpCategoriesSelectionContextValue {
return React.useContext(LpCategoriesSelectionContext);
}

View File

@@ -1,241 +0,0 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { LoadingButton } from '@mui/lab';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import LinearProgress from '@mui/material/LinearProgress';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
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 { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus';
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { paths } from '@/paths';
import { dayjs } from '@/lib/dayjs';
import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table';
import ConfirmDeleteModal from './confirm-delete-modal';
import { useLpCategoriesSelection } from './lp-categories-selection-context';
import type { LpCategory } from './type';
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCategory>[] {
return [
{
formatter: (row): React.JSX.Element => (
<Stack
direction="row"
spacing={1}
sx={{ alignItems: 'center' }}
>
<Link
color="inherit"
component={RouterLink}
href={paths.dashboard.lp_categories.details(row.id)}
sx={{ whiteSpace: 'nowrap' }}
variant="subtitle2"
>
<Stack
direction="row"
spacing={1}
sx={{ alignItems: 'center' }}
>
<Avatar
src={`http://127.0.0.1:8090/api/files/${row.collectionId}/${row.id}/${row.cat_image}`}
variant="rounded"
>
<ImagesIcon size={32} />
</Avatar>{' '}
<div>
<Box sx={{ whiteSpace: 'nowrap' }}>{row.cat_name}</Box>
<Typography
color="text.secondary"
variant="body2"
>
slug: {row.cat_name}
</Typography>
</div>
</Stack>
</Link>
</Stack>
),
name: 'Name',
width: '200px',
},
{
formatter: (row): React.JSX.Element => (
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center' }}
>
<LinearProgress
sx={{ flex: '1 1 auto' }}
value={row.quota}
variant="determinate"
/>
<Typography
color="text.secondary"
variant="body2"
>
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)}
</Typography>
</Stack>
),
// NOTE: please refer to translation.json here
name: 'word-count',
width: '100px',
},
{
formatter: (row): React.JSX.Element => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const mapping = {
active: {
label: 'Active',
icon: (
<CheckCircleIcon
color="var(--mui-palette-success-main)"
weight="fill"
/>
),
},
blocked: { label: 'Blocked', icon: <MinusIcon color="var(--mui-palette-error-main)" /> },
pending: {
label: 'Pending',
icon: (
<ClockIcon
color="var(--mui-palette-warning-main)"
weight="fill"
/>
),
},
NA: {
label: 'NA',
icon: (
<ClockIcon
color="var(--mui-palette-warning-main)"
weight="fill"
/>
),
},
} as const;
const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
return (
<Button
onClick={() => {
toast.error('sorry but not implementd');
}}
style={{ backgroundColor: 'transparent' }}
>
{/* <Chip icon={icon} label={label} size="small" variant="outlined" /> */}
{label}
</Button>
);
},
name: 'Status',
width: '150px',
},
{
formatter(row) {
return dayjs(row.createdAt).format('MMM D, YYYY');
},
name: 'Created at',
width: '100px',
},
{
formatter: (row): React.JSX.Element => (
<Stack
direction="row"
spacing={1}
>
<LoadingButton
//
color="secondary"
component={RouterLink}
href={paths.dashboard.lp_categories.details(row.id)}
>
<PencilSimpleIcon size={24} />
</LoadingButton>
<LoadingButton
color="error"
disabled={row.isEmpty}
onClick={() => {
handleDeleteClick(row.id);
}}
>
<TrashSimpleIcon size={24} />
</LoadingButton>
</Stack>
),
name: 'Actions',
hideName: true,
width: '100px',
align: 'right',
},
];
}
export interface LessonCategoriesTableProps {
rows: LpCategory[];
reloadRows: () => void;
}
export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element {
const { t } = useTranslation(['lp_categories']);
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpCategoriesSelection();
const [idToDelete, setIdToDelete] = React.useState('');
const [open, setOpen] = React.useState(false);
function handleDeleteClick(testId: string): void {
setOpen(true);
setIdToDelete(testId);
}
return (
<React.Fragment>
<ConfirmDeleteModal
idToDelete={idToDelete}
open={open}
reloadRows={reloadRows}
setOpen={setOpen}
/>
<DataTable<LpCategory>
columns={columns(handleDeleteClick)}
onDeselectAll={deselectAll}
onDeselectOne={(_, row) => {
deselectOne(row.id);
}}
onSelectAll={selectAll}
onSelectOne={(_, row) => {
selectOne(row.id);
}}
rows={rows}
selectable
selected={selected}
/>
{!rows.length ? (
<Box sx={{ p: 3 }}>
<Typography
color="text.secondary"
sx={{ textAlign: 'center' }}
variant="body2"
>
{t('no-lesson-categories-found')}
</Typography>
</Box>
) : null}
</React.Fragment>
);
}

View File

@@ -1,419 +0,0 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useRouter } from 'next/navigation';
import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
import { Avatar, Divider, MenuItem, Select } from '@mui/material';
// import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2';
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
// import axios from 'axios';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
import { TextEditor } from '@/components/core/text-editor/text-editor';
import { toast } from '@/components/core/toaster';
import type { CreateFormProps } from './type';
const schema = zod.object({
cat_name: zod.string().min(1, 'name-is-required').max(255),
cat_image: zod.array(zod.any()).optional(),
pos: zod.number().min(1, 'position is required').max(99),
init_answer: zod.string().optional(),
visible: zod.string(),
slug: zod.string().min(1, 'slug-is-required').max(255),
remarks: zod.string().optional(),
description: zod.string().optional(),
// NOTE: for image handling
avatar: zod.string().optional(),
// TODO: remove me
type: zod.string().optional(),
isActive: zod.boolean().optional(),
order: zod.number().optional(),
});
type Values = zod.infer<typeof schema>;
export const defaultValues = {
cat_name: '',
cat_image: undefined,
pos: 1,
init_answer: '',
visible: 'hidden',
slug: '',
remarks: '',
description: '',
} satisfies Values;
export function LpCategoryCreateForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_categories']);
const [isCreating, setIsCreating] = React.useState<boolean>(false);
const {
control,
handleSubmit,
formState: { errors },
setValue,
watch,
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
setIsCreating(true);
const payload: CreateFormProps = {
cat_name: values.cat_name,
cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null,
pos: values.pos,
init_answer: values.init_answer,
visible: values.visible,
slug: values.slug,
remarks: values.remarks,
description: values.description,
//
// TODO: remove me
type: 'type tet',
isActive: true,
order: 1,
};
try {
const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).create(payload);
logger.debug(result);
toast.success(t('create.success'));
router.push(paths.dashboard.lp_categories.list);
} catch (error) {
logger.error(error);
toast.error(t('create.failed'));
} finally {
setIsCreating(false);
}
},
// t is not necessary here
// eslint-disable-next-line react-hooks/exhaustive-deps
[router]
);
const avatarInputRef = React.useRef<HTMLInputElement>(null);
const avatar = watch('avatar');
const handleAvatarChange = React.useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const url = await fileToBase64(file);
setValue('avatar', url);
}
},
[setValue]
);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Card>
<CardContent>
<Stack
divider={<Divider />}
spacing={4}
>
<Stack spacing={3}>
<Typography variant="h6">{t('create.basic-info')}</Typography>
<Grid
container
spacing={3}
>
<Grid xs={12}>
<Stack
direction="row"
spacing={3}
sx={{ alignItems: 'center' }}
>
<Box
sx={{
border: '1px dashed var(--mui-palette-divider)',
borderRadius: '5%',
display: 'inline-flex',
p: '4px',
}}
>
<Avatar
variant="rounded"
src={avatar}
sx={{
'--Avatar-size': '100px',
'--Icon-fontSize': 'var(--icon-fontSize-lg)',
alignItems: 'center',
bgcolor: 'var(--mui-palette-background-level1)',
color: 'var(--mui-palette-text-primary)',
display: 'flex',
justifyContent: 'center',
}}
>
<CameraIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
</Box>
<Stack
spacing={1}
sx={{ alignItems: 'flex-start' }}
>
<Typography variant="subtitle1">{t('create.avatar')}</Typography>
<Typography variant="caption">{t('create.avatarRequirements')}</Typography>
<Button
color="secondary"
onClick={() => {
avatarInputRef.current?.click();
}}
variant="outlined"
>
{t('create.avatar_select')}
</Button>
<input
hidden
onChange={handleAvatarChange}
ref={avatarInputRef}
type="file"
/>
</Stack>
</Stack>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="cat_name"
render={({ field }) => (
<FormControl
disabled={isCreating}
error={Boolean(errors.cat_name)}
fullWidth
>
<InputLabel required>{t('create.cat_name')}</InputLabel>
<OutlinedInput {...field} />
{errors.cat_name ? <FormHelperText>{errors.cat_name.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="pos"
render={({ field }) => (
<FormControl
error={Boolean(errors.pos)}
fullWidth
>
<InputLabel required>{t('create.pos')}</InputLabel>
<OutlinedInput
{...field}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
type="number"
/>
{errors.pos ? <FormHelperText>{errors.pos.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="slug"
render={({ field }) => (
<FormControl
error={Boolean(errors.slug)}
fullWidth
>
<InputLabel required>{t('create.slug')}</InputLabel>
<OutlinedInput {...field} />
{errors.slug ? <FormHelperText>{errors.slug.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="visible"
render={({ field }) => (
<FormControl
error={Boolean(errors.visible)}
fullWidth
>
<InputLabel>{t('edit.visible')}</InputLabel>
<Select {...field}>
<MenuItem value="visible">{t('edit.visible')}</MenuItem>
<MenuItem value="hidden">{t('edit.hidden')}</MenuItem>
</Select>
{errors.visible ? <FormHelperText>{errors.visible.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
{/* */}
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="init_answer"
render={({ field }) => (
<FormControl
error={Boolean(errors.init_answer)}
fullWidth
>
<InputLabel required>{t('create.init_answer')}</InputLabel>
<OutlinedInput {...field} />
{errors.init_answer ? <FormHelperText>{errors.init_answer.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
{/* */}
<Stack spacing={3}>
<Typography variant="h6">{t('create.detail-information')}</Typography>
<Grid
container
spacing={3}
>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="description"
render={({ field }) => {
return (
<Box>
<Typography
variant="subtitle1"
color="text-secondary"
>
{t('create.description')}
</Typography>
<Box sx={{ mt: '8px', '& .tiptap-container': { height: '400px' } }}>
<TextEditor
{...field}
content=""
onUpdate={({ editor }) => {
field.onChange({ target: { value: editor.getHTML() } });
}}
placeholder={t('create.description.default')}
/>
</Box>
</Box>
);
}}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
disabled={isCreating}
control={control}
name="remarks"
render={({ field }) => (
<Box>
<Typography
variant="subtitle1"
color="text.secondary"
>
{t('create.remarks')}
</Typography>
<Box sx={{ mt: '8px', '& .tiptap-container': { height: '400px' } }}>
<TextEditor
content=""
onUpdate={({ editor }) => {
field.onChange({ target: { value: editor.getText() } });
}}
hideToolbar
placeholder={t('create.remarks.default')}
/>
</Box>
</Box>
)}
/>
</Grid>
</Grid>
</Stack>
{/* */}
</Stack>
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}>
<Button
color="secondary"
component={RouterLink}
href={paths.dashboard.lesson_categories.list}
>
{t('create.cancelButton')}
</Button>
<LoadingButton
disabled={isCreating}
loading={isCreating}
type="submit"
variant="contained"
>
{t('create.createButton')}
</LoadingButton>
</CardActions>
</Card>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify({ errors }, null, 2)}</pre>
</Box>
</form>
);
}

View File

@@ -3,9 +3,9 @@
import * as React from 'react';
import { useRouter } from 'next/navigation';
// import { COL_LESSON_CATEGORIES } from '@/constants';
import GetAllCount from '@/db/QuizMFCategories/GetAllCount';
import GetHiddenCount from '@/db/QuizMFCategories/GetHiddenCount';
import GetVisibleCount from '@/db/QuizMFCategories/GetVisibleCount';
import GetAllCount from '@/db/QuizListenings/GetAllCount';
import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount';
import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
@@ -24,7 +24,7 @@ import { FilterButton, FilterPopover, useFilterContext } from '@/components/core
import { Option } from '@/components/core/option';
import { useMfCategoriesSelection } from './mf-categories-selection-context';
import type { MfCategory } from './type';
import { MfCategory } from './type';
export interface Filters {
email?: string;
@@ -37,17 +37,17 @@ export interface Filters {
export type SortDir = 'asc' | 'desc';
export interface LpCategoriesFiltersProps {
export interface MfCategoriesFiltersProps {
filters?: Filters;
sortDir?: SortDir;
fullData: MfCategory[];
}
export function LpCategoriesFilters({
export function MfCategoriesFilters({
filters = {},
sortDir = 'desc',
fullData,
}: LpCategoriesFiltersProps): React.JSX.Element {
}: MfCategoriesFiltersProps): React.JSX.Element {
const { t } = useTranslation();
const { email, phone, status, name, visible, type } = filters;

View File

@@ -16,7 +16,7 @@ interface LessonCategoriesPaginationProps {
rowsPerPage: number;
}
export function LpQuestionsPagination({
export function MfCategoriesPagination({
count,
page,
//

View File

@@ -14,7 +14,7 @@ function noop(): void {
export interface MfCategoriesSelectionContextValue extends Selection {}
export const LpCategoriesSelectionContext = React.createContext<MfCategoriesSelectionContextValue>({
export const MfCategoriesSelectionContext = React.createContext<MfCategoriesSelectionContextValue>({
deselectAll: noop,
deselectOne: noop,
selectAll: noop,
@@ -29,7 +29,7 @@ interface MfCategoriesSelectionProviderProps {
lessonCategories: MfCategory[];
}
export function LpCategoriesSelectionProvider({
export function MfCategoriesSelectionProvider({
children,
lessonCategories = [],
}: MfCategoriesSelectionProviderProps): React.JSX.Element {
@@ -37,10 +37,10 @@ export function LpCategoriesSelectionProvider({
const selection = useSelection(customerIds);
return (
<LpCategoriesSelectionContext.Provider value={{ ...selection }}>{children}</LpCategoriesSelectionContext.Provider>
<MfCategoriesSelectionContext.Provider value={{ ...selection }}>{children}</MfCategoriesSelectionContext.Provider>
);
}
export function useMfCategoriesSelection(): MfCategoriesSelectionContextValue {
return React.useContext(LpCategoriesSelectionContext);
return React.useContext(MfCategoriesSelectionContext);
}

View File

@@ -6,7 +6,6 @@ import { LoadingButton } from '@mui/lab';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import LinearProgress from '@mui/material/LinearProgress';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
@@ -192,8 +191,8 @@ export interface LessonCategoriesTableProps {
reloadRows: () => void;
}
export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element {
const { t } = useTranslation(['lp_categories']);
export function MfCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element {
const { t } = useTranslation(['mf_categories']);
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useMfCategoriesSelection();
const [idToDelete, setIdToDelete] = React.useState('');

View File

@@ -66,9 +66,9 @@ export const defaultValues = {
description: '',
} satisfies Values;
export function LpCategoryCreateForm(): React.JSX.Element {
export function MfCategoryCreateForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_categories']);
const { t } = useTranslation(['mf_categories']);
const [isCreating, setIsCreating] = React.useState<boolean>(false);

View File

@@ -39,7 +39,7 @@ import { TextEditor } from '@/components/core/text-editor/text-editor';
import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading';
import ErrorDisplay from '../error';
import ErrorDisplay from '../../error';
import type { EditFormProps } from './type';
// TODO: review this
@@ -88,9 +88,9 @@ const defaultValues = {
description: '',
} satisfies Values;
export function LpCategoryEditForm(): React.JSX.Element {
export function MfCategoryEditForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_categories']);
const { t } = useTranslation(['mf_categories']);
const { cat_id: catId } = useParams<{ cat_id: string }>();
//

View File

@@ -1,4 +1,4 @@
export interface LpCategory {
export interface MfCategory {
isEmpty?: boolean;
//
id: string;

View File

@@ -1,8 +1,8 @@
import { dayjs } from '@/lib/dayjs';
import { CreateFormProps, LpQuestion } from './type';
import { CreateFormProps, MfQuestion } from './type';
export const defaultLpQuestion: LpQuestion = {
export const defaultMfQuestion: MfQuestion = {
isEmpty: false,
id: 'default-id',
cat_name: 'default-question-name',
@@ -38,7 +38,7 @@ export const defaultLpQuestion: LpQuestion = {
// imageUrl: '',
// };
export const emptyLpQuestion: LpQuestion = {
...defaultLpQuestion,
export const emptyLpQuestion: MfQuestion = {
...defaultMfQuestion,
isEmpty: true,
};

View File

@@ -1,46 +0,0 @@
'use client';
import * as React from 'react';
// import type { LessonCategory } from '@/types/lesson-type';
import { useSelection } from '@/hooks/use-selection';
import type { Selection } from '@/hooks/use-selection';
import type { LpQuestion } from './type';
function noop(): void {
return undefined;
}
export interface LpQuestionsSelectionContextValue extends Selection {}
export const LpQuestionsSelectionContext = React.createContext<LpQuestionsSelectionContextValue>({
deselectAll: noop,
deselectOne: noop,
selectAll: noop,
selectOne: noop,
selected: new Set(),
selectedAny: false,
selectedAll: false,
});
interface LpQuestionsSelectionProviderProps {
children: React.ReactNode;
lessonCategories: LpQuestion[];
}
export function LpQuestionsSelectionProvider({
children,
lessonCategories = [],
}: LpQuestionsSelectionProviderProps): React.JSX.Element {
const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]);
const selection = useSelection(customerIds);
return (
<LpQuestionsSelectionContext.Provider value={{ ...selection }}>{children}</LpQuestionsSelectionContext.Provider>
);
}
export function useLpQuestionsSelection(): LpQuestionsSelectionContextValue {
return React.useContext(LpQuestionsSelectionContext);
}

View File

@@ -3,7 +3,7 @@
import * as React from 'react';
import RouterLink from 'next/link';
import { useRouter } from 'next/navigation';
import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import { COL_QUIZ_MF_CATEGORIES } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
import { Avatar, Divider, MenuItem, Select } from '@mui/material';
@@ -66,7 +66,7 @@ export const defaultValues = {
description: '',
} satisfies Values;
export function LpQuestionCreateForm(): React.JSX.Element {
export function MfQuestionCreateForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_categories']);
@@ -101,11 +101,11 @@ export function LpQuestionCreateForm(): React.JSX.Element {
};
try {
const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).create(payload);
const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).create(payload);
logger.debug(result);
toast.success(t('create.success'));
router.push(paths.dashboard.lp_categories.list);
router.push(paths.dashboard.mf_categories.list);
} catch (error) {
logger.error(error);
toast.error(t('create.failed'));

View File

@@ -4,7 +4,7 @@ import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
//
import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
import { COL_QUIZ_MF_CATEGORIES } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
@@ -88,7 +88,7 @@ const defaultValues = {
description: '',
} satisfies Values;
export function LpQuestionEditForm(): React.JSX.Element {
export function MfQuestionEditForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_categories']);
@@ -129,10 +129,10 @@ export function LpQuestionEditForm(): React.JSX.Element {
};
try {
const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate);
const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).update(catId, tempUpdate);
logger.debug(result);
toast.success(t('edit.success'));
router.push(paths.dashboard.lp_categories.list);
router.push(paths.dashboard.mf_categories.list);
} catch (error) {
logger.error(error);
toast.error(t('update.failed'));
@@ -171,7 +171,7 @@ export function LpQuestionEditForm(): React.JSX.Element {
setShowLoading(true);
try {
const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id);
const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).getOne(id);
reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) });
setTextDescription(result.description);
@@ -480,7 +480,7 @@ export function LpQuestionEditForm(): React.JSX.Element {
<Button
color="secondary"
component={RouterLink}
href={paths.dashboard.lp_categories.list}
href={paths.dashboard.mf_categories.list}
>
{t('edit.cancelButton')}
</Button>

View File

@@ -23,8 +23,8 @@ import { paths } from '@/paths';
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
import { Option } from '@/components/core/option';
import { useLpQuestionsSelection } from './lp-questions-selection-context';
import { LpQuestion } from './type';
import { useMfQuestionsSelection } from './mf-questions-selection-context';
import { MfQuestion } from './type';
export interface Filters {
email?: string;
@@ -37,17 +37,17 @@ export interface Filters {
export type SortDir = 'asc' | 'desc';
export interface LpQuestionsFiltersProps {
export interface MfQuestionsFiltersProps {
filters?: Filters;
sortDir?: SortDir;
fullData: LpQuestion[];
fullData: MfQuestion[];
}
export function LpQuestionsFilters({
export function MfQuestionsFilters({
filters = {},
sortDir = 'desc',
fullData,
}: LpQuestionsFiltersProps): React.JSX.Element {
}: MfQuestionsFiltersProps): React.JSX.Element {
const { t } = useTranslation();
const { email, phone, status, name, visible, type } = filters;
@@ -57,16 +57,16 @@ export function LpQuestionsFilters({
const router = useRouter();
const selection = useLpQuestionsSelection();
const selection = useMfQuestionsSelection();
function getVisible(): number {
return fullData.reduce((count, item: LpQuestion) => {
return fullData.reduce((count, item: MfQuestion) => {
return item.visible === 'visible' ? count + 1 : count;
}, 0);
}
function getHidden(): number {
return fullData.reduce((count, item: LpQuestion) => {
return fullData.reduce((count, item: MfQuestion) => {
return item.visible === 'hidden' ? count + 1 : count;
}, 0);
}
@@ -114,7 +114,7 @@ export function LpQuestionsFilters({
}
// NOTE: modify according to COLLECTION
router.push(`${paths.dashboard.lp_questions.list}?${searchParams.toString()}`);
router.push(`${paths.dashboard.mf_questions.list}?${searchParams.toString()}`);
},
[router]
);

View File

@@ -16,7 +16,7 @@ interface LessonQuestionsPaginationProps {
rowsPerPage: number;
}
export function LpQuestionsPagination({
export function MfQuestionsPagination({
count,
page,
//

View File

@@ -0,0 +1,46 @@
'use client';
import * as React from 'react';
// import type { LessonCategory } from '@/types/lesson-type';
import { useSelection } from '@/hooks/use-selection';
import type { Selection } from '@/hooks/use-selection';
import type { MfQuestion } from './type';
function noop(): void {
return undefined;
}
export interface MfQuestionsSelectionContextValue extends Selection {}
export const MfQuestionsSelectionContext = React.createContext<MfQuestionsSelectionContextValue>({
deselectAll: noop,
deselectOne: noop,
selectAll: noop,
selectOne: noop,
selected: new Set(),
selectedAny: false,
selectedAll: false,
});
interface MfQuestionsSelectionProviderProps {
children: React.ReactNode;
lessonQuestions: MfQuestion[];
}
export function MfQuestionsSelectionProvider({
children,
lessonQuestions = [],
}: MfQuestionsSelectionProviderProps): React.JSX.Element {
const customerIds = React.useMemo(() => lessonQuestions.map((customer) => customer.id), [lessonQuestions]);
const selection = useSelection(customerIds);
return (
<MfQuestionsSelectionContext.Provider value={{ ...selection }}>{children}</MfQuestionsSelectionContext.Provider>
);
}
export function useMfQuestionsSelection(): MfQuestionsSelectionContextValue {
return React.useContext(MfQuestionsSelectionContext);
}

View File

@@ -25,10 +25,10 @@ import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table';
import ConfirmDeleteModal from './confirm-delete-modal';
import { useLpQuestionsSelection } from './lp-questions-selection-context';
import type { LpQuestion } from './type';
import { useMfQuestionsSelection } from './mf-questions-selection-context';
import type { MfQuestion } from './type';
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpQuestion>[] {
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<MfQuestion>[] {
return [
{
formatter: (row): React.JSX.Element => (
@@ -40,7 +40,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpQuest
<Link
color="inherit"
component={RouterLink}
href={paths.dashboard.lp_categories.details(row.id)}
href={paths.dashboard.mf_questions.details(row.id)}
sx={{ whiteSpace: 'nowrap' }}
variant="subtitle2"
>
@@ -163,7 +163,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpQuest
//
color="secondary"
component={RouterLink}
href={paths.dashboard.lp_categories.details(row.id)}
href={paths.dashboard.mf_questions.details(row.id)}
>
<PencilSimpleIcon size={24} />
</LoadingButton>
@@ -187,13 +187,13 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpQuest
}
export interface LessonQuestionsTableProps {
rows: LpQuestion[];
rows: MfQuestion[];
reloadRows: () => void;
}
export function LpQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps): React.JSX.Element {
export function MfQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps): React.JSX.Element {
const { t } = useTranslation(['lp_categories']);
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpQuestionsSelection();
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useMfQuestionsSelection();
const [idToDelete, setIdToDelete] = React.useState('');
const [open, setOpen] = React.useState(false);
@@ -211,7 +211,7 @@ export function LpQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps
reloadRows={reloadRows}
setOpen={setOpen}
/>
<DataTable<LpQuestion>
<DataTable<MfQuestion>
columns={columns(handleDeleteClick)}
onDeselectAll={deselectAll}
onDeselectOne={(_, row) => {

View File

@@ -1,4 +1,4 @@
export interface LpQuestion {
export interface MfQuestion {
isEmpty?: boolean;
//
id: string;

View File

@@ -1 +0,0 @@
please review and add translations, e.g. `{t('[word]')}`

View File

@@ -1,44 +0,0 @@
import { dayjs } from '@/lib/dayjs';
import { CreateFormProps, MfCategory } from './type';
export const defaultLpCategory: MfCategory = {
isEmpty: false,
id: 'default-id',
cat_name: 'default-category-name',
cat_image_url: undefined,
cat_image: undefined,
pos: 0,
visible: 'hidden',
lesson_id: 'default-lesson-id',
description: 'default-description',
remarks: 'default-remarks',
slug: '',
init_answer: {},
// from pocketbase
collectionId: '0000000000',
createdAt: dayjs('2099-01-01').toDate(),
//
name: '',
avatar: '',
email: '',
phone: '',
quota: 0,
status: 'NA',
};
// export const LpCategoryCreateFormDefault: CreateFormProps = {
// name: '',
// type: '',
// pos: 1,
// visible: 'visible',
// description: '',
// isActive: true,
// order: 1,
// imageUrl: '',
// };
export const emptyLpCategory: MfCategory = {
...defaultLpCategory,
isEmpty: true,
};

View File

@@ -1,123 +0,0 @@
'use client';
import * as React from 'react';
import deleteQuizMFCategories from '@/db/QuizMFCategories/Delete';
import { LoadingButton } from '@mui/lab';
import { Button, Container, Modal, Paper } from '@mui/material';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note';
import PocketBase from 'pocketbase';
import { useTranslation } from 'react-i18next';
import { logger } from '@/lib/default-logger';
import { toast } from '@/components/core/toaster';
export default function ConfirmDeleteModal({
open,
setOpen,
idToDelete,
reloadRows,
}: {
open: boolean;
setOpen: (b: boolean) => void;
idToDelete: string;
reloadRows: () => void;
}): React.JSX.Element {
const { t } = useTranslation();
// const handleClose = () => setOpen(false);
function handleClose(): void {
setOpen(false);
}
const [isDeleteing, setIsDeleteing] = React.useState(false);
const style = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
};
function handleUserConfirmDelete(): void {
if (idToDelete) {
setIsDeleteing(true);
deleteQuizMFCategories(idToDelete)
.then(() => {
reloadRows();
handleClose();
toast(t('delete.success'));
})
.catch((err) => {
// console.error(err)
logger.error(err);
toast(t('delete.error'));
})
.finally(() => {
setIsDeleteing(false);
});
}
}
return (
<div>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<Container maxWidth="sm">
<Paper sx={{ border: '1px solid var(--mui-palette-divider)', boxShadow: 'var(--mui-shadows-16)' }}>
<Stack
direction="row"
spacing={2}
sx={{ display: 'flex', p: 3 }}
>
<Avatar sx={{ bgcolor: 'var(--mui-palette-error-50)', color: 'var(--mui-palette-error-main)' }}>
<NoteIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
<Stack spacing={3}>
<Stack spacing={1}>
<Typography variant="h5">{t('Delete Lesson Type ?')}</Typography>
<Typography
color="text.secondary"
variant="body2"
>
{t('Are you sure you want to delete lesson type ?')}
</Typography>
</Stack>
<Stack
direction="row"
spacing={2}
sx={{ justifyContent: 'flex-end' }}
>
<Button
color="secondary"
onClick={handleClose}
>
{t('Cancel')}
</Button>
<LoadingButton
color="error"
variant="contained"
onClick={(e) => {
handleUserConfirmDelete();
}}
loading={isDeleteing}
>
{t('Delete')}
</LoadingButton>
</Stack>
</Stack>
</Stack>
</Paper>
</Container>
</Box>
</Modal>
</div>
);
}

View File

@@ -1,101 +0,0 @@
'use client';
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Chip from '@mui/material/Chip';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple';
import { dayjs } from '@/lib/dayjs';
import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table';
import { Option } from '@/components/core/option';
export interface Notification {
id: string;
type: string;
status: 'delivered' | 'pending' | 'failed';
createdAt: Date;
}
const columns = [
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="inherit">
{row.type}
</Typography>
),
name: 'Type',
width: '300px',
},
{
formatter: (row): React.JSX.Element => {
const mapping = {
delivered: { label: 'Delivered', color: 'success' },
pending: { label: 'Pending', color: 'warning' },
failed: { label: 'Failed', color: 'error' },
} as const;
const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' };
return <Chip color={color} label={label} size="small" variant="soft" />;
},
name: 'Status',
width: '200px',
},
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="inherit">
{dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')}
</Typography>
),
name: 'Date',
align: 'right',
},
] satisfies ColumnDef<Notification>[];
export interface NotificationsProps {
notifications: Notification[];
}
export function Notifications({ notifications }: NotificationsProps): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<EnvelopeSimpleIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Notifications"
/>
<CardContent>
<Stack spacing={3}>
<Stack spacing={2}>
<Select defaultValue="last_invoice" name="type" sx={{ maxWidth: '100%', width: '320px' }}>
<Option value="last_invoice">Resend last invoice</Option>
<Option value="password_reset">Send password reset</Option>
<Option value="verification">Send verification</Option>
</Select>
<div>
<Button startIcon={<EnvelopeSimpleIcon />} variant="contained">
Send email
</Button>
</div>
</Stack>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<Box sx={{ overflowX: 'auto' }}>
<DataTable<Notification> columns={columns} rows={notifications} />
</Box>
</Card>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -1,138 +0,0 @@
'use client';
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple';
import { dayjs } from '@/lib/dayjs';
import type { ColumnDef } from '@/components/core/data-table';
import { DataTable } from '@/components/core/data-table';
export interface Payment {
currency: string;
amount: number;
invoiceId: string;
status: 'pending' | 'completed' | 'canceled' | 'refunded';
createdAt: Date;
}
const columns = [
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="subtitle2">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)}
</Typography>
),
name: 'Amount',
width: '200px',
},
{
formatter: (row): React.JSX.Element => {
const mapping = {
pending: { label: 'Pending', color: 'warning' },
completed: { label: 'Completed', color: 'success' },
canceled: { label: 'Canceled', color: 'error' },
refunded: { label: 'Refunded', color: 'error' },
} as const;
const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' };
return <Chip color={color} label={label} size="small" variant="soft" />;
},
name: 'Status',
width: '200px',
},
{
formatter: (row): React.JSX.Element => {
return <Link variant="inherit">{row.invoiceId}</Link>;
},
name: 'Invoice ID',
width: '150px',
},
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="inherit">
{dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')}
</Typography>
),
name: 'Date',
align: 'right',
},
] satisfies ColumnDef<Payment>[];
export interface PaymentsProps {
ordersValue: number;
payments: Payment[];
refundsValue: number;
totalOrders: number;
}
export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element {
return (
<Card>
<CardHeader
action={
<Button color="secondary" startIcon={<PlusIcon />}>
Create Payment
</Button>
}
avatar={
<Avatar>
<ShoppingCartSimpleIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Payments"
/>
<CardContent>
<Stack spacing={3}>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<Stack
direction="row"
divider={<Divider flexItem orientation="vertical" />}
spacing={3}
sx={{ justifyContent: 'space-between', p: 2 }}
>
<div>
<Typography color="text.secondary" variant="overline">
Total orders
</Typography>
<Typography variant="h6">{new Intl.NumberFormat('en-US').format(totalOrders)}</Typography>
</div>
<div>
<Typography color="text.secondary" variant="overline">
Orders value
</Typography>
<Typography variant="h6">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)}
</Typography>
</div>
<div>
<Typography color="text.secondary" variant="overline">
Refunds
</Typography>
<Typography variant="h6">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)}
</Typography>
</div>
</Stack>
</Card>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<Box sx={{ overflowX: 'auto' }}>
<DataTable<Payment> columns={columns} rows={payments} />
</Box>
</Card>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -1,46 +0,0 @@
import * as React from 'react';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Chip from '@mui/material/Chip';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
export interface Address {
id: string;
country: string;
state: string;
city: string;
zipCode: string;
street: string;
primary?: boolean;
}
export interface ShippingAddressProps {
address: Address;
}
export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement {
return (
<Card sx={{ borderRadius: 1, height: '100%' }} variant="outlined">
<CardContent>
<Stack spacing={2}>
<Typography>
{address.street},
<br />
{address.city}, {address.state}, {address.country},
<br />
{address.zipCode}
</Typography>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'space-between' }}>
{address.primary ? <Chip color="warning" label="Primary" variant="soft" /> : <span />}
<Button color="secondary" size="small" startIcon={<PencilSimpleIcon />}>
Edit
</Button>
</Stack>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -1,61 +0,0 @@
export interface MfCategory {
isEmpty?: boolean;
//
id: string;
collectionId: string;
//
cat_name: string;
cat_image_url?: string;
cat_image?: string;
pos: number;
visible: string;
lesson_id: string;
description: string;
remarks: string;
slug: string;
init_answer: any;
//
name: string;
avatar: string;
email: string;
phone: string;
quota: number;
status: 'pending' | 'active' | 'blocked' | 'NA';
createdAt: Date;
}
export interface CreateFormProps {
cat_name: string;
cat_image: File[] | null;
pos: number;
init_answer?: string;
visible: string;
slug: string;
remarks?: string;
description?: string;
//
// TODO: to remove
type: string;
isActive: boolean;
order: number;
name?: string;
imageUrl?: string;
}
export interface EditFormProps {
cat_name: string;
cat_image: File[] | null;
pos: number;
init_answer: any;
visible: string;
slug: string;
remarks?: string;
description?: string;
//
// TODO: remove below
type: string;
}
export interface Helloworld {
helloworld: string;
}

View File

@@ -1,398 +0,0 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
import Checkbox from '@mui/material/Checkbox';
import Divider from '@mui/material/Divider';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2';
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
import { Controller, useForm } from 'react-hook-form';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { Option } from '@/components/core/option';
import { toast } from '@/components/core/toaster';
function fileToBase64(file: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = () => {
reject(new Error('Error converting file to base64'));
};
});
}
const schema = zod.object({
avatar: zod.string().optional(),
name: zod.string().min(1, 'Name is required').max(255),
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>;
const defaultValues = {
avatar: '',
name: '',
email: '',
phone: '',
company: '',
billingAddress: { country: '', state: '', city: '', zipCode: '', line1: '', line2: '' },
taxId: '',
timezone: 'new_york',
language: 'en',
currency: 'USD',
} satisfies Values;
export function CustomerCreateForm(): React.JSX.Element {
const router = useRouter();
const {
control,
handleSubmit,
formState: { errors },
setValue,
watch,
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(
async (_: Values): Promise<void> => {
try {
// Make API request
toast.success('Customer updated');
router.push(paths.dashboard.customers.details('1'));
} catch (err) {
logger.error(err);
toast.error('Something went wrong!');
}
},
[router]
);
const avatarInputRef = React.useRef<HTMLInputElement>(null);
const avatar = watch('avatar');
const handleAvatarChange = React.useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const url = await fileToBase64(file);
setValue('avatar', url);
}
},
[setValue]
);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Card>
<CardContent>
<Stack divider={<Divider />} spacing={4}>
<Stack spacing={3}>
<Typography variant="h6">Account information</Typography>
<Grid container spacing={3}>
<Grid xs={12}>
<Stack direction="row" spacing={3} sx={{ alignItems: 'center' }}>
<Box
sx={{
border: '1px dashed var(--mui-palette-divider)',
borderRadius: '50%',
display: 'inline-flex',
p: '4px',
}}
>
<Avatar
src={avatar}
sx={{
'--Avatar-size': '100px',
'--Icon-fontSize': 'var(--icon-fontSize-lg)',
alignItems: 'center',
bgcolor: 'var(--mui-palette-background-level1)',
color: 'var(--mui-palette-text-primary)',
display: 'flex',
justifyContent: 'center',
}}
>
<CameraIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
</Box>
<Stack spacing={1} sx={{ alignItems: 'flex-start' }}>
<Typography variant="subtitle1">Avatar</Typography>
<Typography variant="caption">Min 400x400px, PNG or JPEG</Typography>
<Button
color="secondary"
onClick={() => {
avatarInputRef.current?.click();
}}
variant="outlined"
>
Select
</Button>
<input hidden onChange={handleAvatarChange} ref={avatarInputRef} type="file" />
</Stack>
</Stack>
</Grid>
<Grid md={6} xs={12}>
<Controller
control={control}
name="name"
render={({ field }) => (
<FormControl error={Boolean(errors.name)} fullWidth>
<InputLabel required>Name</InputLabel>
<OutlinedInput {...field} />
{errors.name ? <FormHelperText>{errors.name.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid md={6} xs={12}>
<Controller
control={control}
name="email"
render={({ field }) => (
<FormControl error={Boolean(errors.email)} fullWidth>
<InputLabel required>Email address</InputLabel>
<OutlinedInput {...field} type="email" />
{errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null}
</FormControl>
)}
/>
</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>
)}
/>
</Grid>
</Grid>
</Stack>
<Stack spacing={3}>
<Typography variant="h6">Billing information</Typography>
<Grid container spacing={3}>
<Grid md={6} xs={12}>
<Controller
control={control}
name="billingAddress.country"
render={({ field }) => (
<FormControl error={Boolean(errors.billingAddress?.country)} fullWidth>
<InputLabel required>Country</InputLabel>
<Select {...field}>
<Option value="">Choose a country</Option>
<Option value="us">United States</Option>
<Option value="de">Germany</Option>
<Option value="es">Spain</Option>
</Select>
{errors.billingAddress?.country ? (
<FormHelperText>{errors.billingAddress?.country?.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid md={6} xs={12}>
<Controller
control={control}
name="billingAddress.state"
render={({ field }) => (
<FormControl error={Boolean(errors.billingAddress?.state)} fullWidth>
<InputLabel required>State</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.state ? (
<FormHelperText>{errors.billingAddress?.state?.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid md={6} xs={12}>
<Controller
control={control}
name="billingAddress.city"
render={({ field }) => (
<FormControl error={Boolean(errors.billingAddress?.city)} fullWidth>
<InputLabel required>City</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.city ? (
<FormHelperText>{errors.billingAddress?.city?.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid md={6} xs={12}>
<Controller
control={control}
name="billingAddress.zipCode"
render={({ field }) => (
<FormControl error={Boolean(errors.billingAddress?.zipCode)} fullWidth>
<InputLabel required>Zip code</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.zipCode ? (
<FormHelperText>{errors.billingAddress?.zipCode?.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid md={6} xs={12}>
<Controller
control={control}
name="billingAddress.line1"
render={({ field }) => (
<FormControl error={Boolean(errors.billingAddress?.line1)} fullWidth>
<InputLabel required>Address</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.line1 ? (
<FormHelperText>{errors.billingAddress?.line1?.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid md={6} xs={12}>
<Controller
control={control}
name="taxId"
render={({ field }) => (
<FormControl error={Boolean(errors.taxId)} fullWidth>
<InputLabel>Tax ID</InputLabel>
<OutlinedInput {...field} placeholder="e.g EU372054390" />
{errors.taxId ? <FormHelperText>{errors.taxId.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
<Stack spacing={3}>
<Typography variant="h6">Shipping information</Typography>
<FormControlLabel control={<Checkbox defaultChecked />} label="Same as billing address" />
</Stack>
<Stack spacing={3}>
<Typography variant="h6">Additional information</Typography>
<Grid container spacing={3}>
<Grid md={6} xs={12}>
<Controller
control={control}
name="timezone"
render={({ field }) => (
<FormControl error={Boolean(errors.timezone)} fullWidth>
<InputLabel required>Timezone</InputLabel>
<Select {...field}>
<Option value="">Select a timezone</Option>
<Option value="new_york">US - New York</Option>
<Option value="california">US - California</Option>
<Option value="london">UK - London</Option>
</Select>
{errors.timezone ? <FormHelperText>{errors.timezone.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid md={6} xs={12}>
<Controller
control={control}
name="language"
render={({ field }) => (
<FormControl error={Boolean(errors.language)} fullWidth>
<InputLabel required>Language</InputLabel>
<Select {...field}>
<Option value="">Select a language</Option>
<Option value="en">English</Option>
<Option value="es">Spanish</Option>
<Option value="de">German</Option>
</Select>
{errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid md={6} xs={12}>
<Controller
control={control}
name="currency"
render={({ field }) => (
<FormControl error={Boolean(errors.currency)} fullWidth>
<InputLabel>Currency</InputLabel>
<Select {...field}>
<Option value="">Select a currency</Option>
<Option value="USD">USD</Option>
<Option value="EUR">EUR</Option>
<Option value="RON">RON</Option>
</Select>
{errors.currency ? <FormHelperText>{errors.currency.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
</Stack>
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}>
<Button color="secondary" component={RouterLink} href={paths.dashboard.customers.list}>
Cancel
</Button>
<Button type="submit" variant="contained">
Create customer
</Button>
</CardActions>
</Card>
</form>
);
}

View File

@@ -1,241 +0,0 @@
'use client';
import * as React from 'react';
import { useRouter } from 'next/navigation';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
import FormControl from '@mui/material/FormControl';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import type { SelectChangeEvent } from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import Typography from '@mui/material/Typography';
import { paths } from '@/paths';
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
import { Option } from '@/components/core/option';
import { useCustomersSelection } from './customers-selection-context';
// The tabs should be generated using API data.
const tabs = [
{ label: 'All', value: '', count: 5 },
{ label: 'Active', value: 'active', count: 3 },
{ label: 'Pending', value: 'pending', count: 1 },
{ label: 'Blocked', value: 'blocked', count: 1 },
] as const;
export interface Filters {
email?: string;
phone?: string;
status?: string;
}
export type SortDir = 'asc' | 'desc';
export interface CustomersFiltersProps {
filters?: Filters;
sortDir?: SortDir;
}
export function CustomersFilters({ filters = {}, sortDir = 'desc' }: CustomersFiltersProps): React.JSX.Element {
const { email, phone, status } = filters;
const router = useRouter();
const selection = useCustomersSelection();
const updateSearchParams = React.useCallback(
(newFilters: Filters, newSortDir: SortDir): void => {
const searchParams = new URLSearchParams();
if (newSortDir === 'asc') {
searchParams.set('sortDir', newSortDir);
}
if (newFilters.status) {
searchParams.set('status', newFilters.status);
}
if (newFilters.email) {
searchParams.set('email', newFilters.email);
}
if (newFilters.phone) {
searchParams.set('phone', newFilters.phone);
}
router.push(`${paths.dashboard.customers.list}?${searchParams.toString()}`);
},
[router]
);
const handleClearFilters = React.useCallback(() => {
updateSearchParams({}, sortDir);
}, [updateSearchParams, sortDir]);
const handleStatusChange = React.useCallback(
(_: React.SyntheticEvent, value: string) => {
updateSearchParams({ ...filters, status: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleEmailChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, email: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handlePhoneChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, phone: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleSortChange = React.useCallback(
(event: SelectChangeEvent) => {
updateSearchParams(filters, event.target.value as SortDir);
},
[updateSearchParams, filters]
);
const hasFilters = status || email || phone;
return (
<div>
<Tabs onChange={handleStatusChange} sx={{ px: 3 }} value={status ?? ''} variant="scrollable">
{tabs.map((tab) => (
<Tab
icon={<Chip label={tab.count} size="small" variant="soft" />}
iconPosition="end"
key={tab.value}
label={tab.label}
sx={{ minHeight: 'auto' }}
tabIndex={0}
value={tab.value}
/>
))}
</Tabs>
<Divider />
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap', px: 3, py: 2 }}>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto', flexWrap: 'wrap' }}>
<FilterButton
displayValue={email}
label="Email"
onFilterApply={(value) => {
handleEmailChange(value as string);
}}
onFilterDelete={() => {
handleEmailChange();
}}
popover={<EmailFilterPopover />}
value={email}
/>
<FilterButton
displayValue={phone}
label="Phone number"
onFilterApply={(value) => {
handlePhoneChange(value as string);
}}
onFilterDelete={() => {
handlePhoneChange();
}}
popover={<PhoneFilterPopover />}
value={phone}
/>
{hasFilters ? <Button onClick={handleClearFilters}>Clear filters</Button> : null}
</Stack>
{selection.selectedAny ? (
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<Typography color="text.secondary" variant="body2">
{selection.selected.size} selected
</Typography>
<Button color="error" variant="contained">
Delete
</Button>
</Stack>
) : null}
<Select name="sort" onChange={handleSortChange} sx={{ maxWidth: '100%', width: '120px' }} value={sortDir}>
<Option value="desc">Newest</Option>
<Option value="asc">Oldest</Option>
</Select>
</Stack>
</div>
);
}
function EmailFilterPopover(): React.JSX.Element {
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title="Filter by email">
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
Apply
</Button>
</FilterPopover>
);
}
function PhoneFilterPopover(): React.JSX.Element {
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title="Filter by phone number">
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
Apply
</Button>
</FilterPopover>
);
}

View File

@@ -1,31 +0,0 @@
'use client';
import * as React from 'react';
import TablePagination from '@mui/material/TablePagination';
function noop(): void {
return undefined;
}
interface CustomersPaginationProps {
count: number;
page: number;
}
export function CustomersPagination({ count, page }: CustomersPaginationProps): React.JSX.Element {
// You should implement the pagination using a similar logic as the filters.
// Note that when page change, you should keep the filter search params.
return (
<TablePagination
component="div"
count={count}
onPageChange={noop}
onRowsPerPageChange={noop}
page={page}
rowsPerPage={5}
rowsPerPageOptions={[5, 10, 25]}
//
/>
);
}

Some files were not shown because too many files have changed in this diff Show More