Compare commits

...

3 Commits

Author SHA1 Message Date
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
louiscklaw
1c3dccd68e update pocketbase seeding, 2025-04-22 02:53:49 +08:00
66 changed files with 2803 additions and 123 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

@@ -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';
@@ -57,7 +58,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
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 };
@@ -147,6 +148,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

@@ -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';
@@ -202,7 +203,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>
);
}

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

@@ -7,7 +7,7 @@ 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 { 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/lp-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,26 @@ 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 {
// TODO: modify from lp_categories to mf_categories
const { t } = useTranslation(['lp_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 +45,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,11 +58,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_CATEGORIES)
.collection(COL_QUIZ_MF_CATEGORIES)
.getList(currentPage + 1, rowsPerPage, {});
const { items, totalItems } = models;
const tempLessonTypes: LpCategory[] = items.map((lt) => {
return { ...defaultLpCategory, ...lt };
const tempLessonTypes: MfCategory[] = items.map((lt) => {
return { ...defaultMfCategory, ...lt };
});
setLessonCategoriesData(tempLessonTypes);
@@ -113,7 +115,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 +124,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 +147,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 +168,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

@@ -7,7 +7,7 @@ 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 { 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 { 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())) {

View File

@@ -66,7 +66,7 @@ export const defaultValues = {
description: '',
} satisfies Values;
export function LpQuestionCreateForm(): React.JSX.Element {
export function LpCategoryCreateForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_categories']);

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

@@ -66,7 +66,7 @@ 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']);
@@ -105,7 +105,7 @@ export function LpCategoryCreateForm(): React.JSX.Element {
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

@@ -0,0 +1,456 @@
'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 './mf-categories-selection-context';
import { MfCategory } 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: MfCategory[];
}
export function MfCategoriesFilters({
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: MfCategory) => {
return item.visible === 'visible' ? count + 1 : count;
}, 0);
}
function getHidden(): number {
return fullData.reduce((count, item: MfCategory) => {
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.mf_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

@@ -0,0 +1,49 @@
'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 MfCategoriesPagination({
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

@@ -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 { MfCategory } 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: MfCategory[];
}
export function MfCategoriesSelectionProvider({
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

@@ -0,0 +1,241 @@
'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 './mf-categories-selection-context';
import type { MfCategory } from './type';
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<MfCategory>[] {
return [
{
formatter: (row): React.JSX.Element => (
<Stack
direction="row"
spacing={1}
sx={{ alignItems: 'center' }}
>
<Link
color="inherit"
component={RouterLink}
href={paths.dashboard.mf_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.mf_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: MfCategory[];
reloadRows: () => void;
}
export function MfCategoriesTable({ 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<MfCategory>
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

@@ -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 MfCategoryEditForm(): 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

@@ -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

@@ -0,0 +1,419 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useRouter } from 'next/navigation';
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';
// 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 MfQuestionCreateForm(): 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_MF_CATEGORIES).create(payload);
logger.debug(result);
toast.success(t('create.success'));
router.push(paths.dashboard.mf_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

@@ -0,0 +1,500 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
//
import { COL_QUIZ_MF_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 MfQuestionEditForm(): 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_MF_CATEGORIES).update(catId, tempUpdate);
logger.debug(result);
toast.success(t('edit.success'));
router.push(paths.dashboard.mf_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_MF_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.mf_categories.list}
>
{t('edit.cancelButton')}
</Button>
<LoadingButton
disabled={isUpdating}
loading={isUpdating}
type="submit"
variant="contained"
>
{t('edit.updateButton')}
</LoadingButton>
</CardActions>
</Card>
</form>
);
}

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 { useLpQuestionsSelection } from './mf-questions-selection-context';
import { MfQuestion } from './type';
export interface Filters {
email?: string;
@@ -40,10 +40,10 @@ export type SortDir = 'asc' | 'desc';
export interface LpQuestionsFiltersProps {
filters?: Filters;
sortDir?: SortDir;
fullData: LpQuestion[];
fullData: MfQuestion[];
}
export function LpQuestionsFilters({
export function MfQuestionsFilters({
filters = {},
sortDir = 'desc',
fullData,
@@ -60,13 +60,13 @@ export function LpQuestionsFilters({
const selection = useLpQuestionsSelection();
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);
}

View File

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

View File

@@ -6,7 +6,7 @@ import * as React from 'react';
import { useSelection } from '@/hooks/use-selection';
import type { Selection } from '@/hooks/use-selection';
import type { LpQuestion } from './type';
import type { MfQuestion } from './type';
function noop(): void {
return undefined;
@@ -26,14 +26,14 @@ export const LpQuestionsSelectionContext = React.createContext<LpQuestionsSelect
interface LpQuestionsSelectionProviderProps {
children: React.ReactNode;
lessonCategories: LpQuestion[];
lessonQuestions: MfQuestion[];
}
export function LpQuestionsSelectionProvider({
export function MfQuestionsSelectionProvider({
children,
lessonCategories = [],
lessonQuestions = [],
}: LpQuestionsSelectionProviderProps): React.JSX.Element {
const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]);
const customerIds = React.useMemo(() => lessonQuestions.map((customer) => customer.id), [lessonQuestions]);
const selection = useSelection(customerIds);
return (

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 { useLpQuestionsSelection } 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,11 +187,11 @@ 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();
@@ -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

@@ -15,6 +15,7 @@ const COL_QUIZ_LP_CATEGORIES = 'QuizLPCategories';
const COL_QUIZ_LP_QUESTIONS = 'QuizLPQuestions';
//
const COL_QUIZ_MF_CATEGORIES = 'QuizMFCategories';
const COL_QUIZ_MF_QUESTIONS = 'QuizMFQuestions';
export {
COL_LESSON_TYPES,
@@ -29,5 +30,6 @@ export {
COL_QUIZ_LP_QUESTIONS,
//
COL_QUIZ_MF_CATEGORIES,
COL_QUIZ_MF_QUESTIONS,
//
};

View File

@@ -0,0 +1,3 @@
{
"editor.formatOnSave": false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

View File

@@ -22,6 +22,11 @@ $app.rootCmd.addCommand(
//
require(`${__hooks}/seed/020_QuizLPCategories.js`)($app);
require(`${__hooks}/seed/021_QuizLPQuestions.js`)($app);
//
require(`${__hooks}/seed/030_QuizMFCategories.js`)($app);
require(`${__hooks}/seed/031_QuizMFQuestions.js`)($app);
//
require(`${__hooks}/seed/040_QuizCRCategories.js`)($app);
$app.reloadCachedCollections();
$app.reloadSettings();

View File

@@ -0,0 +1,14 @@
module.exports = ($app) => {
console.log("004 clean user table start");
dirtyTruncateTable("Users");
dirtyTruncateTable("UserMetas");
console.log("004 clean user table done");
};
const dirtyTruncateTable = (COLLECTION_NAME) => {
console.log(`perform dirty method to truncate table "${COLLECTION_NAME}"`);
const cmd_to_exec = $os.cmd("sqlite3", "/pb_data/data.db", `DELETE from ${COLLECTION_NAME};`);
cmd_to_exec.output();
};

View File

@@ -0,0 +1,81 @@
module.exports = ($app) => {
const ASSETS_DIR = "/pb_hooks/assets";
const getAsset = (name) => $filesystem.fileFromPath(ASSETS_DIR + "/" + name);
const id_v = "1".padStart(15, 0); //id_vocabulary
const id_c = "2".padStart(15, 0); //id_connectives
let row_array = [
["11".padStart(15, 0), "teacher1@123.com", "teacher1@123.com", "teacher1@123.com", true, true, "test_user_1"],
["12".padStart(15, 0), "teacher2@123.com", "teacher2@123.com", "teacher2@123.com", true, true, "test_user_2"],
["13".padStart(15, 0), "teacher3@123.com", "teacher3@123.com", "teacher3@123.com", true, true, "test_user_3"],
];
let um_row_array = [
[
"11".padStart(15, 0),
"teacher1@123.com",
"active",
"11".padStart(15, 0),
JSON.stringify({}),
getAsset("people1.png"),
"teacher",
//
],
[
"12".padStart(15, 0),
"teacher2@123.com",
"active",
"12".padStart(15, 0),
JSON.stringify({}),
getAsset("people2.png"),
"teacher",
//
],
[
"13".padStart(15, 0),
"teacher3@123.com",
"active",
"13".padStart(15, 0),
JSON.stringify({}),
getAsset("people3.png"),
"teacher",
//
],
];
let users_collection = $app.findCollectionByNameOrId("users");
let user_metas_collection = $app.findCollectionByNameOrId("UserMetas");
for (let i = 0; i < row_array.length; i++) {
let user = row_array[i];
let um = um_row_array[i];
let record = new Record(users_collection);
record.set("id", user[0]);
record.set("password", user[1]);
record.set("passwordConfirm", user[2]);
record.set("email", user[3]);
record.set("emailVisibility", user[4]);
record.set("verified", user[5]);
record.set("name", user[6]);
$app.save(record);
let um_record = new Record(user_metas_collection);
um_record.set("id", um[0]);
um_record.set("helloworld", um[1]);
um_record.set("state", um[2]);
um_record.set("user_id", um[3]);
um_record.set("meta", um[4]);
um_record.set("avatar", um[5]);
um_record.set("role", um[6]);
$app.save(um_record);
}
console.log("005 add teacher user done");
};
const dirtyTruncateTable = (COLLECTION_NAME) => {
console.log(`perform dirty method to truncate table "${COLLECTION_NAME}"`);
const cmd_to_exec = $os.cmd("sqlite3", "/pb_data/data.db", `DELETE from ${COLLECTION_NAME};`);
cmd_to_exec.output();
};

View File

@@ -0,0 +1,58 @@
module.exports = ($app) => {
const ASSETS_DIR = "/pb_hooks/assets";
const getAsset = (name) => $filesystem.fileFromPath(ASSETS_DIR + "/" + name);
const id_v = "1".padStart(15, 0); //id_vocabulary
const id_c = "2".padStart(15, 0); //id_connectives
let row_array = [
["1".padStart(15, 0), "user1@123.com", "user1@123.com", "user1@123.com", true, true, "test_user_1"],
["2".padStart(15, 0), "user2@123.com", "user2@123.com", "user2@123.com", true, true, "test_user_2"],
["3".padStart(15, 0), "user3@123.com", "user3@123.com", "user3@123.com", true, true, "test_user_3"],
["4".padStart(15, 0), "user4@123.com", "user4@123.com", "user4@123.com", true, true, "test_user_4"],
["5".padStart(15, 0), "user5@123.com", "user5@123.com", "user5@123.com", true, true, "test_user_5"],
];
let um_row_array = [
["1".padStart(15, 0), "user1@123.com", "active", "1".padStart(15, 0), JSON.stringify({}), getAsset("people1.png"), "student"],
["2".padStart(15, 0), "user2@123.com", "active", "2".padStart(15, 0), JSON.stringify({}), getAsset("people2.png"), "student"],
["3".padStart(15, 0), "user3@123.com", "active", "3".padStart(15, 0), JSON.stringify({}), getAsset("people3.png"), "student"],
["4".padStart(15, 0), "user4@123.com", "active", "4".padStart(15, 0), JSON.stringify({}), getAsset("people4.png"), "student"],
["5".padStart(15, 0), "user5@123.com", "active", "5".padStart(15, 0), JSON.stringify({}), getAsset("people5.png"), "student"],
];
let users_collection = $app.findCollectionByNameOrId("users");
let user_metas_collection = $app.findCollectionByNameOrId("UserMetas");
for (let i = 0; i < row_array.length; i++) {
let user = row_array[i];
let um = um_row_array[i];
let record = new Record(users_collection);
record.set("id", user[0]);
record.set("password", user[1]);
record.set("passwordConfirm", user[2]);
record.set("email", user[3]);
record.set("emailVisibility", user[4]);
record.set("verified", user[5]);
record.set("name", user[6]);
$app.save(record);
let um_record = new Record(user_metas_collection);
um_record.set("id", um[0]);
um_record.set("helloworld", um[1]);
um_record.set("state", um[2]);
um_record.set("user_id", um[3]);
um_record.set("meta", um[4]);
um_record.set("avatar", um[5]);
um_record.set("role", um[6]);
$app.save(um_record);
}
console.log("006 add student user done");
};
const dirtyTruncateTable = (COLLECTION_NAME) => {
console.log(`perform dirty method to truncate table "${COLLECTION_NAME}"`);
const cmd_to_exec = $os.cmd("sqlite3", "/pb_data/data.db", `DELETE from ${COLLECTION_NAME};`);
cmd_to_exec.output();
};

View File

@@ -0,0 +1,43 @@
module.exports = ($app) => {
const ASSETS_DIR = "/pb_hooks/assets";
const getAsset = (name) => $filesystem.fileFromPath(ASSETS_DIR + "/" + name);
const id_v = "1".padStart(15, 0); //id_vocabulary
const id_c = "2".padStart(15, 0); //id_connectives
const getId = (id) => id.padStart(15, 0);
let row_array = [
[getId("1"), "news (listening)", getAsset("ci_news.jpg"), 1, {}, "visible"],
[getId("2"), "sports (listening)", getAsset("ci_sports.jpg"), 2, {}, "visible"],
[getId("3"), "technology (listening)", getAsset("ci_technology.jpg"), 3, {}, "visible"],
[getId("4"), "art (listening)", getAsset("ci_art.jpg"), 4, {}, "visible"],
[getId("5"), "basic (listening)", getAsset("ci_basic.jpg"), 5, {}, "visible"],
[getId("6"), "nature (listening)", getAsset("ci_nature.jpg"), 6, {}, "visible"],
[getId("7"), "workplace (listening)", getAsset("ci_workplace.jpg"), 7, {}, "visible"],
[getId("8"), "workplace (listening)", getAsset("ci_workplace.jpg"), 8, {}, "visible"],
[getId("99"), "test hidden (listening)", getAsset("ci_workplace.jpg"), 9, {}, "hidden"],
];
dirtyTruncateTable("QuizLPCategories");
let lt_collection = $app.findCollectionByNameOrId("QuizLPCategories");
for (let i = 0; i < row_array.length; i++) {
let lesson_type = row_array[i];
let record = new Record(lt_collection);
record.set("id", lesson_type[0]);
record.set("cat_name", lesson_type[1]);
record.set("cat_image", lesson_type[2]);
record.set("pos", lesson_type[3]);
record.set("init_answer", lesson_type[4]);
record.set("visible", lesson_type[5]);
$app.save(record);
}
console.log(`020_QuizLPCategories done`);
};
const dirtyTruncateTable = (COLLECTION_NAME) => {
console.log(`perform dirty method to truncate table "${COLLECTION_NAME}"`);
const cmd_to_exec = $os.cmd("sqlite3", "/pb_data/data.db", `DELETE from ${COLLECTION_NAME};`);
cmd_to_exec.output();
};

View File

@@ -0,0 +1,47 @@
module.exports = ($app) => {
const ASSETS_DIR = "/pb_hooks/assets";
const getAsset = (name) => $filesystem.fileFromPath(ASSETS_DIR + "/" + name);
const id_v = "1".padStart(15, 0); //id_vocabulary
const id_c = "2".padStart(15, 0); //id_connectives
const cat_id_technology = "3".padStart(15, 0);
const getId = (id) => id.padStart(15, 0);
let row_array = [
[getId("1") ,"news (LP)" ,getAsset("ci_news.jpg") ,1 ,{} ,"visible" ,"news" ,getAsset("keyboard.mp3") ,cat_id_technology] ,
[getId("2") ,"sports (LP)" ,getAsset("ci_sports.jpg") ,2 ,{} ,"visible" ,"sports" ,getAsset("mouse.mp3") ,cat_id_technology] ,
[getId("3") ,"technology (LP)" ,getAsset("ci_technology.jpg") ,3 ,{} ,"visible" ,"technology" ,getAsset("keyboard.mp3") ,cat_id_technology] ,
[getId("4") ,"art (LP)" ,getAsset("ci_art.jpg") ,4 ,{} ,"visible" ,"art" ,getAsset("mouse.mp3") ,cat_id_technology] ,
[getId("5") ,"basic (LP)" ,getAsset("ci_basic.jpg") ,5 ,{} ,"visible" ,"basic" ,getAsset("keyboard.mp3") ,cat_id_technology] ,
[getId("6") ,"nature (LP)" ,getAsset("ci_nature.jpg") ,6 ,{} ,"visible" ,"nature" ,getAsset("keyboard.mp3") ,cat_id_technology] ,
[getId("7") ,"workplace (LP)" ,getAsset("ci_workplace.jpg") ,7 ,{} ,"visible" ,"workplace" ,getAsset("keyboard.mp3") ,cat_id_technology] ,
[getId("8") ,"workplace (LP)" ,getAsset("ci_workplace.jpg") ,8 ,{} ,"visible" ,"workplace" ,getAsset("keyboard.mp3") ,cat_id_technology] ,
[getId("99") ,"test hidden (LP)" ,getAsset("ci_workplace.jpg") ,9 ,{} ,"hidden" ,"test" ,getAsset("keyboard.mp3") ,cat_id_technology] ,
];
dirtyTruncateTable("QuizLPQuestions");
let lt_collection = $app.findCollectionByNameOrId("QuizLPQuestions");
for (let i = 0; i < row_array.length; i++) {
let lesson_type = row_array[i];
let record = new Record(lt_collection);
record.set("id", lesson_type[0]);
record.set("cat_name", lesson_type[1]);
record.set("cat_image", lesson_type[2]);
record.set("pos", lesson_type[3]);
record.set("init_answer", lesson_type[4]);
record.set("visible", lesson_type[5]);
record.set("word", lesson_type[6]);
record.set("sound", lesson_type[7]);
record.set("cat_id", lesson_type[7]);
$app.save(record);
}
console.log(`021_QuizLPQuestions done`);
};
const dirtyTruncateTable = (COLLECTION_NAME) => {
console.log(`perform dirty method to truncate table "${COLLECTION_NAME}"`);
const cmd_to_exec = $os.cmd("sqlite3", "/pb_data/data.db", `DELETE from ${COLLECTION_NAME};`);
cmd_to_exec.output();
};

View File

@@ -0,0 +1,35 @@
module.exports = ($app) => {
const ASSETS_DIR = "/pb_hooks/assets";
const getAsset = (name) => $filesystem.fileFromPath(ASSETS_DIR + "/" + name);
const getId = (id) => id.padStart(15, 0);
const id_v = getId("1"); //id_vocabulary
const id_c = getId("2"); //id_connectives
let row_array = [
[getId("1"), "keyboard", getAsset("keyboard.jpg"), getId("1")],
[getId("2"), "mouse", getAsset("mouse.jpg"), getId("1")],
];
dirtyTruncateTable("QuizLPQuestions");
let lt_collection = $app.findCollectionByNameOrId("QuizLPQuestions");
for (let i = 0; i < row_array.length; i++) {
let lesson_type = row_array[i];
let record = new Record(lt_collection);
record.set("id", lesson_type[0]);
record.set("word", lesson_type[1]);
record.set("sound", lesson_type[2]);
record.set("cat_id", lesson_type[3]);
$app.save(record);
}
console.log(`021 QuizLPQuestions done`);
};
const dirtyTruncateTable = (COLLECTION_NAME) => {
console.log(`perform dirty method to truncate table "${COLLECTION_NAME}"`);
const cmd_to_exec = $os.cmd("sqlite3", "/pb_data/data.db", `DELETE from ${COLLECTION_NAME};`);
cmd_to_exec.output();
};

View File

@@ -0,0 +1,43 @@
module.exports = ($app) => {
const ASSETS_DIR = "/pb_hooks/assets";
const getAsset = (name) => $filesystem.fileFromPath(ASSETS_DIR + "/" + name);
const id_v = "1".padStart(15, 0); //id_vocabulary
const id_c = "2".padStart(15, 0); //id_connectives
const getId = (id) => id.padStart(15, 0);
let row_array = [
[getId("1"), "news (matching)", getAsset("ci_news.jpg"), 1, {}, "visible"],
[getId("2"), "sports (matching)", getAsset("ci_sports.jpg"), 2, {}, "visible"],
[getId("3"), "technology (matching)", getAsset("ci_technology.jpg"), 3, {}, "visible"],
[getId("4"), "art (matching)", getAsset("ci_art.jpg"), 4, {}, "visible"],
[getId("5"), "basic (matching)", getAsset("ci_basic.jpg"), 5, {}, "visible"],
[getId("6"), "nature (matching)", getAsset("ci_nature.jpg"), 6, {}, "visible"],
[getId("7"), "workplace (matching)", getAsset("ci_workplace.jpg"), 7, {}, "visible"],
[getId("8"), "workplace (matching)", getAsset("ci_workplace.jpg"), 8, {}, "visible"],
[getId("99"), "test hidden (matching)", getAsset("ci_workplace.jpg"), 9, {}, "hidden"],
];
dirtyTruncateTable("QuizMFCategories");
let lt_collection = $app.findCollectionByNameOrId("QuizMFCategories");
for (let i = 0; i < row_array.length; i++) {
let lesson_type = row_array[i];
let record = new Record(lt_collection);
record.set("id", lesson_type[0]);
record.set("cat_name", lesson_type[1]);
record.set("cat_image", lesson_type[2]);
record.set("pos", lesson_type[3]);
record.set("init_answer", lesson_type[4]);
record.set("visible", lesson_type[5]);
$app.save(record);
}
console.log(`030_QuizMFCategories done`);
};
const dirtyTruncateTable = (COLLECTION_NAME) => {
console.log(`perform dirty method to truncate table "${COLLECTION_NAME}"`);
const cmd_to_exec = $os.cmd("sqlite3", "/pb_data/data.db", `DELETE from ${COLLECTION_NAME};`);
cmd_to_exec.output();
};

View File

@@ -0,0 +1,43 @@
module.exports = ($app) => {
const ASSETS_DIR = "/pb_hooks/assets";
const getAsset = (name) => $filesystem.fileFromPath(ASSETS_DIR + "/" + name);
const id_v = "1".padStart(15, 0); //id_vocabulary
const id_c = "2".padStart(15, 0); //id_connectives
const getId = (id) => id.padStart(15, 0);
let row_array = [
[getId("1"), "news (MF)", getAsset("ci_news.jpg"), 1, {}, "visible"],
[getId("2"), "sports (MF)", getAsset("ci_sports.jpg"), 2, {}, "visible"],
[getId("3"), "technology (MF)", getAsset("ci_technology.jpg"), 3, {}, "visible"],
[getId("4"), "art (MF)", getAsset("ci_art.jpg"), 4, {}, "visible"],
[getId("5"), "basic (MF)", getAsset("ci_basic.jpg"), 5, {}, "visible"],
[getId("6"), "nature (MF)", getAsset("ci_nature.jpg"), 6, {}, "visible"],
[getId("7"), "workplace (MF)", getAsset("ci_workplace.jpg"), 7, {}, "visible"],
[getId("8"), "workplace (MF)", getAsset("ci_workplace.jpg"), 8, {}, "visible"],
[getId("99"), "test hidden (MF)", getAsset("ci_workplace.jpg"), 9, {}, "hidden"],
];
dirtyTruncateTable("QuizMFQuestions");
let lt_collection = $app.findCollectionByNameOrId("QuizMFQuestions");
for (let i = 0; i < row_array.length; i++) {
let lesson_type = row_array[i];
let record = new Record(lt_collection);
record.set("id", lesson_type[0]);
record.set("cat_name", lesson_type[1]);
record.set("cat_image", lesson_type[2]);
record.set("pos", lesson_type[3]);
record.set("init_answer", lesson_type[4]);
record.set("visible", lesson_type[5]);
$app.save(record);
}
console.log(`031_QuizMFQuestions done`);
};
const dirtyTruncateTable = (COLLECTION_NAME) => {
console.log(`perform dirty method to truncate table "${COLLECTION_NAME}"`);
const cmd_to_exec = $os.cmd("sqlite3", "/pb_data/data.db", `DELETE from ${COLLECTION_NAME};`);
cmd_to_exec.output();
};

View File

@@ -0,0 +1,43 @@
module.exports = ($app) => {
const ASSETS_DIR = "/pb_hooks/assets";
const getAsset = (name) => $filesystem.fileFromPath(ASSETS_DIR + "/" + name);
const id_v = "1".padStart(15, 0); //id_vocabulary
const id_c = "2".padStart(15, 0); //id_connectives
const getId = (id) => id.padStart(15, 0);
let row_array = [
[getId("1"), "news (connect)", getAsset("ci_news.jpg"), 1, {}, "visible"],
[getId("2"), "sports (connect)", getAsset("ci_sports.jpg"), 2, {}, "visible"],
[getId("3"), "technology (connect)", getAsset("ci_technology.jpg"), 3, {}, "visible"],
[getId("4"), "art (connect)", getAsset("ci_art.jpg"), 4, {}, "visible"],
[getId("5"), "basic (connect)", getAsset("ci_basic.jpg"), 5, {}, "visible"],
[getId("6"), "nature (connect)", getAsset("ci_nature.jpg"), 6, {}, "visible"],
[getId("7"), "workplace (connect)", getAsset("ci_workplace.jpg"), 7, {}, "visible"],
[getId("8"), "workplace (connect)", getAsset("ci_workplace.jpg"), 8, {}, "visible"],
[getId("99"), "test hidden (connect)", getAsset("ci_workplace.jpg"), 9, {}, "hidden"],
];
dirtyTruncateTable("QuizMFCategories");
let lt_collection = $app.findCollectionByNameOrId("QuizMFCategories");
for (let i = 0; i < row_array.length; i++) {
let lesson_type = row_array[i];
let record = new Record(lt_collection);
record.set("id", lesson_type[0]);
record.set("cat_name", lesson_type[1]);
record.set("cat_image", lesson_type[2]);
record.set("pos", lesson_type[3]);
record.set("init_answer", lesson_type[4]);
record.set("visible", lesson_type[5]);
$app.save(record);
}
console.log(`030_QuizMFCategories done`);
};
const dirtyTruncateTable = (COLLECTION_NAME) => {
console.log(`perform dirty method to truncate table "${COLLECTION_NAME}"`);
const cmd_to_exec = $os.cmd("sqlite3", "/pb_data/data.db", `DELETE from ${COLLECTION_NAME};`);
cmd_to_exec.output();
};

View File

@@ -1356,6 +1356,20 @@
"presentable": false,
"system": false,
"type": "autodate"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}
],
"indexes": [],

View File

@@ -0,0 +1,29 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1305841361")
// add field
collection.fields.addAt(7, new Field({
"hidden": false,
"id": "file376926767",
"maxSelect": 1,
"maxSize": 0,
"mimeTypes": [],
"name": "avatar",
"presentable": false,
"protected": false,
"required": false,
"system": false,
"thumbs": [],
"type": "file"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1305841361")
// remove field
collection.fields.removeById("file376926767")
return app.save(collection)
})

View File

@@ -0,0 +1,29 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1305841361")
// add field
collection.fields.addAt(8, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "text1466534506",
"max": 0,
"min": 0,
"name": "role",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1305841361")
// remove field
collection.fields.removeById("text1466534506")
return app.save(collection)
})

View File

@@ -0,0 +1,29 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_3639453778")
// add field
collection.fields.addAt(5, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_3639453778")
// remove field
collection.fields.removeById("text2058414169")
return app.save(collection)
})

View File

@@ -0,0 +1,29 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_3639453778")
// add field
collection.fields.addAt(8, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "text2560465762",
"max": 0,
"min": 0,
"name": "slug",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_3639453778")
// remove field
collection.fields.removeById("text2560465762")
return app.save(collection)
})

View File

@@ -0,0 +1,28 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1305841361")
// update collection data
unmarshal({
"createRule": "",
"deleteRule": "",
"listRule": "",
"updateRule": "",
"viewRule": ""
}, collection)
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1305841361")
// update collection data
unmarshal({
"createRule": null,
"deleteRule": null,
"listRule": null,
"updateRule": null,
"viewRule": null
}, collection)
return app.save(collection)
})

View File

@@ -0,0 +1,45 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_3639453778")
// add field
collection.fields.addAt(9, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "text1156222427",
"max": 0,
"min": 0,
"name": "remarks",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
// add field
collection.fields.addAt(10, new Field({
"convertURLs": false,
"hidden": false,
"id": "editor1843675174",
"maxSize": 0,
"name": "description",
"presentable": false,
"required": false,
"system": false,
"type": "editor"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_3639453778")
// remove field
collection.fields.removeById("text1156222427")
// remove field
collection.fields.removeById("editor1843675174")
return app.save(collection)
})

View File

@@ -0,0 +1,29 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_84667061")
// add field
collection.fields.addAt(5, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_84667061")
// remove field
collection.fields.removeById("text2058414169")
return app.save(collection)
})

View File

@@ -0,0 +1,153 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_742947356")
// add field
collection.fields.addAt(4, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "text1125157303",
"max": 0,
"min": 0,
"name": "cat_name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
// add field
collection.fields.addAt(5, new Field({
"hidden": false,
"id": "file2034676914",
"maxSelect": 1,
"maxSize": 0,
"mimeTypes": [],
"name": "cat_image",
"presentable": false,
"protected": false,
"required": false,
"system": false,
"thumbs": [],
"type": "file"
}))
// add field
collection.fields.addAt(6, new Field({
"hidden": false,
"id": "number2161764012",
"max": null,
"min": null,
"name": "pos",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
}))
// add field
collection.fields.addAt(7, new Field({
"hidden": false,
"id": "json3915970527",
"maxSize": 0,
"name": "init_answer",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}))
// add field
collection.fields.addAt(8, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "text2058414169",
"max": 0,
"min": 0,
"name": "visible",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
// add field
collection.fields.addAt(9, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "text2560465762",
"max": 0,
"min": 0,
"name": "slug",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
// add field
collection.fields.addAt(10, new Field({
"autogeneratePattern": "",
"hidden": false,
"id": "text1156222427",
"max": 0,
"min": 0,
"name": "remarks",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}))
// add field
collection.fields.addAt(11, new Field({
"convertURLs": false,
"hidden": false,
"id": "editor1843675174",
"maxSize": 0,
"name": "description",
"presentable": false,
"required": false,
"system": false,
"type": "editor"
}))
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_742947356")
// remove field
collection.fields.removeById("text1125157303")
// remove field
collection.fields.removeById("file2034676914")
// remove field
collection.fields.removeById("number2161764012")
// remove field
collection.fields.removeById("json3915970527")
// remove field
collection.fields.removeById("text2058414169")
// remove field
collection.fields.removeById("text2560465762")
// remove field
collection.fields.removeById("text1156222427")
// remove field
collection.fields.removeById("editor1843675174")
return app.save(collection)
})

View File

@@ -0,0 +1,28 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_1305841361")
// update collection data
unmarshal({
"createRule": "",
"deleteRule": "",
"listRule": "",
"updateRule": "",
"viewRule": ""
}, collection)
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1305841361")
// update collection data
unmarshal({
"createRule": null,
"deleteRule": null,
"listRule": null,
"updateRule": null,
"viewRule": null
}, collection)
return app.save(collection)
})