From 00a978e55a116f3c3711b296b33dbd209ed7d536 Mon Sep 17 00:00:00 2001 From: louiscklaw Date: Mon, 21 Apr 2025 12:15:25 +0800 Subject: [PATCH] update, --- 002_source/cms/next.config.mjs | 7 +- .../cms/src/app/dashboard/customers/page.tsx | 31 +- .../src/app/dashboard/lesson_types/page.tsx | 8 +- .../[cat_id]/BasicDetailCard.tsx | 79 +++ .../lp/questions.plan/[cat_id]/TitleCard.tsx | 73 +++ .../lp/questions.plan/[cat_id]/page.tsx | 138 +++++ .../dashboard/lp/questions.plan/_PROMPT.md | 17 + .../lp/questions.plan/create/page.tsx | 53 ++ .../questions.plan/edit/[cat_id]/_PROMPT.md | 11 + .../lp/questions.plan/edit/[cat_id]/page.tsx | 53 ++ .../app/dashboard/lp/questions.plan/page.tsx | 213 ++++++++ .../dashboard/lp/questions/[cat_id]/page.tsx | 24 +- .../dashboard/lp/questions/create/page.tsx | 6 +- .../lp/questions/edit/[cat_id]/page.tsx | 8 +- .../src/app/dashboard/lp/questions/page.tsx | 119 +++-- .../src/components/dashboard/error/index.tsx | 17 +- .../dashboard/lp/questions/_PROMPT.MD | 20 + .../dashboard/lp/questions/_constants.ts | 10 +- .../questions/lp-question-create-form.tsx} | 6 +- .../questions/lp-question-edit-form.tsx} | 6 +- .../lp/questions/lp-questions-filters.tsx | 456 ++++++++++++++++ .../lp/questions/lp-questions-pagination.tsx | 49 ++ .../lp-questions-selection-context.tsx | 46 ++ .../questions/lp-questions-table.tsx} | 12 +- .../dashboard/lp/questions/type.d.ts | 16 +- .../dashboard/lp_questions.plan/_PROMPT.MD | 1 + .../dashboard/lp_questions.plan/_constants.ts | 44 ++ .../confirm-delete-modal.tsx | 122 +++++ .../lp-question-create-form.tsx | 419 +++++++++++++++ .../lp-question-edit-form.tsx | 499 ++++++++++++++++++ .../lp-questions-filters.tsx} | 2 +- .../lp-questions-pagination.tsx} | 0 .../lp-questions-selection-context.tsx} | 0 .../lp-questions-table.tsx} | 2 +- .../lp_questions.plan/notifications.tsx | 101 ++++ .../dashboard/lp_questions.plan/payments.tsx | 138 +++++ .../lp_questions.plan/shipping-address.tsx | 46 ++ .../dashboard/lp_questions.plan/type.d.ts | 61 +++ .../dashboard/lp_questions/_constants.ts | 8 +- .../lp_questions/confirm-delete-modal.tsx | 7 +- .../lp-question-create-form.tsx} | 12 +- .../lp-question-edit-form.tsx} | 14 +- ...s-filters.tsx => lp-questions-filters.tsx} | 14 +- ...nation.tsx => lp-questions-pagination.tsx} | 2 +- ...tsx => lp-questions-selection-context.tsx} | 8 +- .../lp_questions/lp-questions-table.tsx | 241 +++++++++ .../dashboard/lp_questions/type.d.ts | 2 +- 002_source/cms/src/db/DB_AI_GUIDELINE.MD | 21 +- .../cms/src/db/QuizLPCategories/Create.tsx | 12 + .../cms/src/db/QuizLPCategories/Delete.tsx | 7 + .../cms/src/db/QuizLPCategories/GetAll.tsx | 8 + .../src/db/QuizLPCategories/GetAllCount.tsx | 9 + .../cms/src/db/QuizLPCategories/GetById.tsx | 8 + .../db/QuizLPCategories/GetHiddenCount.tsx | 14 + .../db/QuizLPCategories/GetVisibleCount.tsx | 14 + .../cms/src/db/QuizLPCategories/Update.tsx | 9 + .../cms/src/db/QuizLPCategories/_PROMPT.md | 18 + .../cms/src/db/QuizLPQuestions/Create.tsx | 12 + .../cms/src/db/QuizLPQuestions/Delete.tsx | 7 + .../cms/src/db/QuizLPQuestions/GetAll.tsx | 8 + .../src/db/QuizLPQuestions/GetAllCount.tsx | 9 + .../cms/src/db/QuizLPQuestions/GetById.tsx | 8 + .../src/db/QuizLPQuestions/GetHiddenCount.tsx | 14 + .../db/QuizLPQuestions/GetVisibleCount.tsx | 14 + .../cms/src/db/QuizLPQuestions/Update.tsx | 9 + .../cms/src/db/QuizLPQuestions/_PROMPT.md | 18 + 002_source/cms/src/db/schema.json | 119 ++++- 67 files changed, 3423 insertions(+), 136 deletions(-) create mode 100644 002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/BasicDetailCard.tsx create mode 100644 002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/TitleCard.tsx create mode 100644 002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/page.tsx create mode 100644 002_source/cms/src/app/dashboard/lp/questions.plan/_PROMPT.md create mode 100644 002_source/cms/src/app/dashboard/lp/questions.plan/create/page.tsx create mode 100644 002_source/cms/src/app/dashboard/lp/questions.plan/edit/[cat_id]/_PROMPT.md create mode 100644 002_source/cms/src/app/dashboard/lp/questions.plan/edit/[cat_id]/page.tsx create mode 100644 002_source/cms/src/app/dashboard/lp/questions.plan/page.tsx rename 002_source/cms/src/components/dashboard/{lp_questions/lp-category-create-form.tsx => lp/questions/lp-question-create-form.tsx} (98%) rename 002_source/cms/src/components/dashboard/{lp_questions/lp-category-edit-form.tsx => lp/questions/lp-question-edit-form.tsx} (98%) create mode 100644 002_source/cms/src/components/dashboard/lp/questions/lp-questions-filters.tsx create mode 100644 002_source/cms/src/components/dashboard/lp/questions/lp-questions-pagination.tsx create mode 100644 002_source/cms/src/components/dashboard/lp/questions/lp-questions-selection-context.tsx rename 002_source/cms/src/components/dashboard/{lp_questions/lp-categories-table.tsx => lp/questions/lp-questions-table.tsx} (95%) create mode 100644 002_source/cms/src/components/dashboard/lp_questions.plan/_PROMPT.MD create mode 100644 002_source/cms/src/components/dashboard/lp_questions.plan/_constants.ts create mode 100644 002_source/cms/src/components/dashboard/lp_questions.plan/confirm-delete-modal.tsx create mode 100644 002_source/cms/src/components/dashboard/lp_questions.plan/lp-question-create-form.tsx create mode 100644 002_source/cms/src/components/dashboard/lp_questions.plan/lp-question-edit-form.tsx rename 002_source/cms/src/components/dashboard/{lp/questions/lp-categories-filters.tsx => lp_questions.plan/lp-questions-filters.tsx} (99%) rename 002_source/cms/src/components/dashboard/{lp/questions/lp-categories-pagination.tsx => lp_questions.plan/lp-questions-pagination.tsx} (100%) rename 002_source/cms/src/components/dashboard/{lp/questions/lp-categories-selection-context.tsx => lp_questions.plan/lp-questions-selection-context.tsx} (100%) rename 002_source/cms/src/components/dashboard/{lp/questions/lp-categories-table.tsx => lp_questions.plan/lp-questions-table.tsx} (98%) create mode 100644 002_source/cms/src/components/dashboard/lp_questions.plan/notifications.tsx create mode 100644 002_source/cms/src/components/dashboard/lp_questions.plan/payments.tsx create mode 100644 002_source/cms/src/components/dashboard/lp_questions.plan/shipping-address.tsx create mode 100644 002_source/cms/src/components/dashboard/lp_questions.plan/type.d.ts rename 002_source/cms/src/components/dashboard/{lp/questions/lp-category-create-form.tsx => lp_questions/lp-question-create-form.tsx} (97%) rename 002_source/cms/src/components/dashboard/{lp/questions/lp-category-edit-form.tsx => lp_questions/lp-question-edit-form.tsx} (97%) rename 002_source/cms/src/components/dashboard/lp_questions/{lp-categories-filters.tsx => lp-questions-filters.tsx} (96%) rename 002_source/cms/src/components/dashboard/lp_questions/{lp-categories-pagination.tsx => lp-questions-pagination.tsx} (96%) rename 002_source/cms/src/components/dashboard/lp_questions/{lp-categories-selection-context.tsx => lp-questions-selection-context.tsx} (88%) create mode 100644 002_source/cms/src/components/dashboard/lp_questions/lp-questions-table.tsx create mode 100644 002_source/cms/src/db/QuizLPCategories/Create.tsx create mode 100644 002_source/cms/src/db/QuizLPCategories/Delete.tsx create mode 100644 002_source/cms/src/db/QuizLPCategories/GetAll.tsx create mode 100644 002_source/cms/src/db/QuizLPCategories/GetAllCount.tsx create mode 100644 002_source/cms/src/db/QuizLPCategories/GetById.tsx create mode 100644 002_source/cms/src/db/QuizLPCategories/GetHiddenCount.tsx create mode 100644 002_source/cms/src/db/QuizLPCategories/GetVisibleCount.tsx create mode 100644 002_source/cms/src/db/QuizLPCategories/Update.tsx create mode 100644 002_source/cms/src/db/QuizLPCategories/_PROMPT.md create mode 100644 002_source/cms/src/db/QuizLPQuestions/Create.tsx create mode 100644 002_source/cms/src/db/QuizLPQuestions/Delete.tsx create mode 100644 002_source/cms/src/db/QuizLPQuestions/GetAll.tsx create mode 100644 002_source/cms/src/db/QuizLPQuestions/GetAllCount.tsx create mode 100644 002_source/cms/src/db/QuizLPQuestions/GetById.tsx create mode 100644 002_source/cms/src/db/QuizLPQuestions/GetHiddenCount.tsx create mode 100644 002_source/cms/src/db/QuizLPQuestions/GetVisibleCount.tsx create mode 100644 002_source/cms/src/db/QuizLPQuestions/Update.tsx create mode 100644 002_source/cms/src/db/QuizLPQuestions/_PROMPT.md diff --git a/002_source/cms/next.config.mjs b/002_source/cms/next.config.mjs index 1308494..61a75e7 100644 --- a/002_source/cms/next.config.mjs +++ b/002_source/cms/next.config.mjs @@ -2,7 +2,12 @@ const config = { reactStrictMode: false, images: { - domains: ['example.com', '127.0.0.1', 'localhost'], + domains: [ + // + 'example.com', + '127.0.0.1', + 'localhost', + ], }, }; diff --git a/002_source/cms/src/app/dashboard/customers/page.tsx b/002_source/cms/src/app/dashboard/customers/page.tsx index 552f8dd..efb61b4 100644 --- a/002_source/cms/src/app/dashboard/customers/page.tsx +++ b/002_source/cms/src/app/dashboard/customers/page.tsx @@ -1,5 +1,7 @@ +'use client'; + import * as React from 'react'; -import type { Metadata } from 'next'; +// import type { Metadata } from 'next'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Card from '@mui/material/Card'; @@ -17,7 +19,7 @@ import { CustomersSelectionProvider } from '@/components/dashboard/customer/cust import { CustomersTable } from '@/components/dashboard/customer/customers-table'; import type { Customer } from '@/components/dashboard/customer/customers-table'; -export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; +// export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; const customers = [ { @@ -182,6 +184,10 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { const sortedCustomers = applySort(customers, sortDir); const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status }); + React.useEffect(() => { + console.log('helloworld'); + }, []); + return ( - + Customers - - + - + diff --git a/002_source/cms/src/app/dashboard/lesson_types/page.tsx b/002_source/cms/src/app/dashboard/lesson_types/page.tsx index 58434c2..97fc0ac 100644 --- a/002_source/cms/src/app/dashboard/lesson_types/page.tsx +++ b/002_source/cms/src/app/dashboard/lesson_types/page.tsx @@ -40,7 +40,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { const [isLoadingAddPage, setIsLoadingAddPage] = React.useState(false); const [showLoading, setShowLoading] = React.useState(true); - const [showError, setShowError] = React.useState(false); + const [showError, setShowError] = React.useState({ show: false, detail: '' }); // const [rowsPerPage, setRowsPerPage] = React.useState(5); const [f, setF] = React.useState([]); @@ -65,6 +65,8 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { setF(tempLessonTypes); } catch (error) { // + logger.error(error); + setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); } finally { setShowLoading(false); } @@ -106,9 +108,9 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { if (showError) return ( ); diff --git a/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/BasicDetailCard.tsx b/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/BasicDetailCard.tsx new file mode 100644 index 0000000..bf02e80 --- /dev/null +++ b/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/BasicDetailCard.tsx @@ -0,0 +1,79 @@ +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Card from '@mui/material/Card'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import IconButton from '@mui/material/IconButton'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; +import { useTranslation } from 'react-i18next'; + +import { PropertyItem } from '@/components/core/property-item'; +import { PropertyList } from '@/components/core/property-list'; +import { LpQuestion } from '@/components/dashboard/lp_questions.plan/type'; + +export default function BasicDetailCard({ + lpModel: model, + handleEditClick, +}: { + lpModel: LpQuestion; + handleEditClick: () => void; +}): React.JSX.Element { + const { t } = useTranslation(); + + return ( + + { + handleEditClick(); + }} + > + + + } + avatar={ + + + + } + title={t('list.basic-details')} + /> + } + orientation="vertical" + sx={{ '--PropertyItem-padding': '12px 24px' }} + > + {( + [ + { + key: 'Customer ID', + value: ( + + ), + }, + { key: 'Name', value: model.question_name }, + { key: 'Remarks', value: model.remarks }, + { key: 'Description', value: model.description }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + ); +} diff --git a/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/TitleCard.tsx b/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/TitleCard.tsx new file mode 100644 index 0000000..67deaac --- /dev/null +++ b/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/TitleCard.tsx @@ -0,0 +1,73 @@ +'use client'; + +import * as React from 'react'; +import { Button } from '@mui/material'; +import Avatar from '@mui/material/Avatar'; +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { useTranslation } from 'react-i18next'; + +import { LpQuestion } from '@/components/dashboard/lp_questions.plan/type'; + +function getImageUrlFrRecord(record: LpQuestion): string { + return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.question_image}`; +} + +export default function SampleTitleCard({ lpModel }: { lpModel: LpQuestion }): React.JSX.Element { + const { t } = useTranslation(); + + return ( + <> + + + {t('empty')} + +
+ + {lpModel.question_name} + + } + label={lpModel.visible} + size="small" + variant="outlined" + /> + + + {lpModel.slug} + +
+
+
+ +
+ + ); +} diff --git a/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/page.tsx new file mode 100644 index 0000000..f22c443 --- /dev/null +++ b/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/page.tsx @@ -0,0 +1,138 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; +import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; +import SamplePaymentCard from '@/app/dashboard/Sample/SamplePaymentCard'; +import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard'; +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Grid from '@mui/material/Unstable_Grid2'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import type { RecordModel } from 'pocketbase'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +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.plan/_constants'; +import { Notifications } from '@/components/dashboard/lp_questions.plan/notifications'; +import type { LpQuestion } from '@/components/dashboard/lp_questions.plan/type'; +import FormLoading from '@/components/loading'; + +import BasicDetailCard from './BasicDetailCard'; +import TitleCard from './TitleCard'; + +export default function Page(): React.JSX.Element { + const { t } = useTranslation(); + const router = useRouter(); + // + const { cat_id: catId } = useParams<{ cat_id: string }>(); + // + const [showLoading, setShowLoading] = React.useState(true); + const [showError, setShowError] = React.useState({ show: false, detail: '' }); + + // + const [showLessonQuestion, setShowLessonQuestion] = React.useState(defaultLpQuestion); + + function handleEditClick() { + router.push(paths.dashboard.lp_questions.edit(showLessonQuestion.id)); + } + + React.useEffect(() => { + if (catId) { + pb.collection(COL_QUIZ_LP_QUESTIONS) + .getOne(catId) + .then((model: RecordModel) => { + setShowLessonQuestion({ ...defaultLpQuestion, ...model }); + }) + .catch((err) => { + logger.error(err); + toast(t('list.error')); + + setShowError({ show: true, detail: JSON.stringify(err) }); + }) + .finally(() => { + setShowLoading(false); + }); + } + }, [catId]); + + if (showLoading) return ; + if (showError.show) + return ( + + ); + + return ( + + + +
+ + + {t('list.title')} + +
+ + + +
+ + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/lp/questions.plan/_PROMPT.md b/002_source/cms/src/app/dashboard/lp/questions.plan/_PROMPT.md new file mode 100644 index 0000000..6d8b030 --- /dev/null +++ b/002_source/cms/src/app/dashboard/lp/questions.plan/_PROMPT.md @@ -0,0 +1,17 @@ +please help to review the `tsx` file in this folder +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lp/questions` + +it was clone from +`category`/`categories`, `lp_category`/`lp_categories` +please help to modify to `question`/`questions`, `lp_question`/`lp_questions` + +please also help to modify the name of +`variables`, `constants`, `functions`, `classes`, components's name, paths + +the db fields structures are the same + +do not move the files +do not create directories +keep current folder structure is important + +thanks diff --git a/002_source/cms/src/app/dashboard/lp/questions.plan/create/page.tsx b/002_source/cms/src/app/dashboard/lp/questions.plan/create/page.tsx new file mode 100644 index 0000000..f83acf2 --- /dev/null +++ b/002_source/cms/src/app/dashboard/lp/questions.plan/create/page.tsx @@ -0,0 +1,53 @@ +'use client'; + +// RULES: +// T.B.A. +// +import * as React from 'react'; +import RouterLink from 'next/link'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { LpQuestionCreateForm } from '@/components/dashboard/lp_questions.plan/lp-question-create-form'; + +export default function Page(): React.JSX.Element { + // RULES: follow the name of page directory + const { t } = useTranslation(['lp_questions']); + + return ( + + + +
+ + + {t('title')} + +
+
+ {t('create.title')} +
+
+ +
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/lp/questions.plan/edit/[cat_id]/_PROMPT.md b/002_source/cms/src/app/dashboard/lp/questions.plan/edit/[cat_id]/_PROMPT.md new file mode 100644 index 0000000..abf4465 --- /dev/null +++ b/002_source/cms/src/app/dashboard/lp/questions.plan/edit/[cat_id]/_PROMPT.md @@ -0,0 +1,11 @@ +# task + +## instruction + +with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/_helloworld/page.tsx` + +with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_types/edit/[typeId]/page.tsx` + +please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_categories/edit/page.tsx` + +please draft a tsx for showing error to user thanks, diff --git a/002_source/cms/src/app/dashboard/lp/questions.plan/edit/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/lp/questions.plan/edit/[cat_id]/page.tsx new file mode 100644 index 0000000..45a9b7a --- /dev/null +++ b/002_source/cms/src/app/dashboard/lp/questions.plan/edit/[cat_id]/page.tsx @@ -0,0 +1,53 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { LpQuestionEditForm } from '@/components/dashboard/lp_questions.plan/lp-question-edit-form'; + +export default function Page(): React.JSX.Element { + const { t } = useTranslation(['lp_questions']); + + React.useEffect(() => { + // console.log('helloworld'); + }, []); + + return ( + + + +
+ + + {t('edit.title')} + +
+
+ {t('edit.title')} +
+
+ +
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/lp/questions.plan/page.tsx b/002_source/cms/src/app/dashboard/lp/questions.plan/page.tsx new file mode 100644 index 0000000..edf1f1d --- /dev/null +++ b/002_source/cms/src/app/dashboard/lp/questions.plan/page.tsx @@ -0,0 +1,213 @@ +'use client'; + +// RULES: +// contains list page for lp_questions (QuizLPQuestions) +// contain definition to collection only +// +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import { LoadingButton } from '@mui/lab'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Divider from '@mui/material/Divider'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import type { ListResult, RecordModel } from 'pocketbase'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +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.plan/_constants'; +import { LpQuestionsFilters } from '@/components/dashboard/lp_questions.plan/lp-questions-filters'; +import type { Filters } from '@/components/dashboard/lp_questions.plan/lp-questions-filters'; +import { LpQuestionsPagination } from '@/components/dashboard/lp_questions.plan/lp-questions-pagination'; +import { LpQuestionsSelectionProvider } from '@/components/dashboard/lp_questions.plan/lp-questions-selection-context'; +import { LpQuestionsTable } from '@/components/dashboard/lp_questions.plan/lp-questions-table'; +import type { LpQuestion } from '@/components/dashboard/lp_questions.plan/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, setLessonQuestionsData] = React.useState([]); + // + + const [isLoadingAddPage, setIsLoadingAddPage] = React.useState(false); + const [showLoading, setShowLoading] = React.useState(true); + const [showError, setShowError] = React.useState({ show: false, detail: '' }); + // + const [rowsPerPage, setRowsPerPage] = React.useState(5); + const [filteredQuestions, setFilteredQuestions] = React.useState([]); + const [currentPage, setCurrentPage] = React.useState(1); + const [recordCount, setRecordCount] = React.useState(0); + const [listOption, setListOption] = React.useState({}); + const [listSort, setListSort] = React.useState({}); + + // + const sortedLessonQuestions = applySort(lessonQuestionsData, sortDir); + const filteredLessonQuestions = applyFilters(sortedLessonQuestions, { email, phone, status }); + + const reloadRows = async (): Promise => { + try { + const models: ListResult = await pb + .collection(COL_QUIZ_LP_QUESTIONS) + .getList(currentPage + 1, rowsPerPage, {}); + const { items, totalItems } = models; + const tempLessonTypes: LpQuestion[] = items.map((lt) => { + return { ...defaultLpQuestion, ...lt }; + }); + + setLessonQuestionsData(tempLessonTypes); + setRecordCount(totalItems); + setF(tempLessonTypes); + console.log({ currentPage, f }); + } catch (error) { + // + setShowError({ show: true, detail: JSON.stringify(error) }); + } finally { + setShowLoading(false); + } + }; + + React.useEffect(() => { + void reloadRows(); + }, [currentPage, rowsPerPage, listOption]); + + if (showLoading) return ; + + if (showError.show) + return ( + + ); + + return ( + + + + + {t('list.title')} + + + { + setIsLoadingAddPage(true); + router.push(paths.dashboard.lp_questions.create); + }} + startIcon={} + variant="contained" + > + {t('list.add')} + + + + + + + + + + + + + + + + + ); +} + +// Sorting and filtering has to be done on the server. + +function applySort(row: LpQuestion[], sortDir: 'asc' | 'desc' | undefined): LpQuestion[] { + return row.sort((a, b) => { + if (sortDir === 'asc') { + return a.createdAt.getTime() - b.createdAt.getTime(); + } + + return b.createdAt.getTime() - a.createdAt.getTime(); + }); +} + +function applyFilters(row: LpQuestion[], { email, phone, status, name, visible }: Filters): LpQuestion[] { + return row.filter((item) => { + if (email) { + if (!item.email?.toLowerCase().includes(email.toLowerCase())) { + return false; + } + } + + if (phone) { + if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { + return false; + } + } + + if (status) { + if (item.status !== status) { + return false; + } + } + + if (name) { + if (!item.name?.toLowerCase().includes(name.toLowerCase())) { + return false; + } + } + + if (visible) { + if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) { + return false; + } + } + + return true; + }); +} + +interface PageProps { + searchParams: { + email?: string; + phone?: string; + sortDir?: 'asc' | 'desc'; + status?: string; + name?: string; + visible?: string; + type?: string; + // + }; +} diff --git a/002_source/cms/src/app/dashboard/lp/questions/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/lp/questions/[cat_id]/page.tsx index cab19fa..0e8ba78 100644 --- a/002_source/cms/src/app/dashboard/lp/questions/[cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions/[cat_id]/page.tsx @@ -7,11 +7,11 @@ 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_LP_QUESTIONS } from '@/constants'; +import { Grid } from '@mui/material'; import Box from '@mui/material/Box'; import Link from '@mui/material/Link'; import Stack from '@mui/material/Stack'; -import Grid from '@mui/material/Unstable_Grid2'; import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; import type { RecordModel } from 'pocketbase'; import { useTranslation } from 'react-i18next'; @@ -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 { defaultLpQuestion } from '@/components/dashboard/lp_questions/_constants.ts'; +import { Notifications } from '@/components/dashboard/lp_questions/notifications'; +import type { LpQuestion } from '@/components/dashboard/lp_questions/type'; import FormLoading from '@/components/loading'; import BasicDetailCard from './BasicDetailCard'; @@ -39,18 +39,18 @@ export default function Page(): React.JSX.Element { const [showError, setShowError] = React.useState({ show: false, detail: '' }); // - const [showLessonCategory, setShowLessonCategory] = React.useState(defaultLpCategory); + const [showLessonQuestion, setShowLessonQuestion] = React.useState(defaultLpQuestion); function handleEditClick() { - router.push(paths.dashboard.lp_categories.edit(showLessonCategory.id)); + router.push(paths.dashboard.lp_questions.edit(showLessonQuestion.id)); } React.useEffect(() => { if (catId) { - pb.collection(COL_QUIZ_LP_CATEGORIES) + pb.collection(COL_QUIZ_LP_QUESTIONS) .getOne(catId) .then((model: RecordModel) => { - setShowLessonCategory({ ...defaultLpCategory, ...model }); + setShowLessonQuestion({ ...defaultLpQuestion, ...model }); }) .catch((err) => { logger.error(err); @@ -94,7 +94,7 @@ export default function Page(): React.JSX.Element { variant="subtitle2" > - {t('list.title')} + {t('edit.title')} - +
diff --git a/002_source/cms/src/app/dashboard/lp/questions/create/page.tsx b/002_source/cms/src/app/dashboard/lp/questions/create/page.tsx index c874d65..548260f 100644 --- a/002_source/cms/src/app/dashboard/lp/questions/create/page.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions/create/page.tsx @@ -13,11 +13,11 @@ 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 { LpQuestionCreateForm } from '@/components/dashboard/lp_questions/lp-question-create-form'; export default function Page(): React.JSX.Element { // RULES: follow the name of page directory - const { t } = useTranslation(['lp_categories']); + const { t } = useTranslation(['lp_questions']); return ( {t('create.title')} - +
); diff --git a/002_source/cms/src/app/dashboard/lp/questions/edit/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/lp/questions/edit/[cat_id]/page.tsx index 9f7f258..1ae2cae 100644 --- a/002_source/cms/src/app/dashboard/lp/questions/edit/[cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions/edit/[cat_id]/page.tsx @@ -10,10 +10,10 @@ 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 { LpQuestionEditForm } from '@/components/dashboard/lp_questions/lp-question-edit-form'; export default function Page(): React.JSX.Element { - const { t } = useTranslation(['lp_categories']); + const { t } = useTranslation(['lp_questions']); React.useEffect(() => { // console.log('helloworld'); @@ -34,7 +34,7 @@ export default function Page(): React.JSX.Element { @@ -46,7 +46,7 @@ export default function Page(): React.JSX.Element { {t('edit.title')} - + ); diff --git a/002_source/cms/src/app/dashboard/lp/questions/page.tsx b/002_source/cms/src/app/dashboard/lp/questions/page.tsx index 24a908c..ca2c0b6 100644 --- a/002_source/cms/src/app/dashboard/lp/questions/page.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions/page.tsx @@ -1,12 +1,12 @@ 'use client'; // RULES: -// contains list page for lp_categories (QuizLPCategories) +// contains list page for lp_questions (QuizLPQuestions) // contain definition to collection only // import * as React from 'react'; import { useRouter } from 'next/navigation'; -import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; import { LoadingButton } from '@mui/lab'; import Box from '@mui/material/Box'; import Card from '@mui/material/Card'; @@ -22,20 +22,20 @@ 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 { defaultLpQuestion } from '@/components/dashboard/lp_questions/_constants'; +import { LpQuestionsFilters } from '@/components/dashboard/lp_questions/lp-questions-filters'; +import type { Filters } from '@/components/dashboard/lp_questions/lp-questions-filters'; +import { LpQuestionsPagination } from '@/components/dashboard/lp_questions/lp-questions-pagination'; +import { LpQuestionsSelectionProvider } from '@/components/dashboard/lp_questions/lp-questions-selection-context'; +import { LpQuestionsTable } from '@/components/dashboard/lp_questions/lp-questions-table'; +import type { LpQuestion } from '@/components/dashboard/lp_questions/type'; import FormLoading from '@/components/loading'; export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { t } = useTranslation(['lp_categories']); + const { t } = useTranslation(['lp_questions']); const { email, phone, sortDir, status, name, visible, type } = searchParams; const router = useRouter(); - const [lessonCategoriesData, setLessonCategoriesData] = React.useState([]); + const [lessonQuestionsData, setLessonCategoriesData] = React.useState([]); // const [isLoadingAddPage, setIsLoadingAddPage] = React.useState(false); @@ -43,49 +43,105 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { const [showError, setShowError] = React.useState({ show: false, detail: '' }); // const [rowsPerPage, setRowsPerPage] = React.useState(5); - const [f, setF] = React.useState([]); - const [currentPage, setCurrentPage] = React.useState(1); + const [f, setF] = React.useState([]); + const [currentPage, setCurrentPage] = React.useState(0); const [recordCount, setRecordCount] = React.useState(0); const [listOption, setListOption] = React.useState({}); const [listSort, setListSort] = React.useState({}); // - const sortedLessonCategories = applySort(lessonCategoriesData, sortDir); + const sortedLessonCategories = applySort(lessonQuestionsData, sortDir); const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status }); const reloadRows = async (): Promise => { try { const models: ListResult = await pb - .collection(COL_QUIZ_LP_CATEGORIES) - .getList(currentPage + 1, rowsPerPage, {}); + .collection(COL_QUIZ_LP_QUESTIONS) + .getList(currentPage + 1, rowsPerPage, listOption); const { items, totalItems } = models; - const tempLessonTypes: LpCategory[] = items.map((lt) => { - return { ...defaultLpCategory, ...lt }; + const tempLessonTypes: LpQuestion[] = items.map((lt) => { + return { ...defaultLpQuestion, ...lt }; }); setLessonCategoriesData(tempLessonTypes); setRecordCount(totalItems); setF(tempLessonTypes); - console.log({ currentPage, f }); + // console.log({ currentPage, f }); } catch (error) { // - setShowError({ show: true, detail: JSON.stringify(error) }); + logger.error(error); + setShowError({ + // + show: true, + code: error.status, + detail: JSON.stringify(error, null, 2), + }); } finally { setShowLoading(false); } }; + const [lastListOption, setLastListOption] = React.useState({}); + const isFirstRun = React.useRef(false); React.useEffect(() => { - void reloadRows(); + if (!isFirstRun.current) { + isFirstRun.current = true; + } else { + if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { + // reset page number as tab changes + setLastListOption(listOption); + setCurrentPage(0); + void reloadRows(); + } else { + void reloadRows(); + } + } }, [currentPage, rowsPerPage, listOption]); + React.useEffect(() => { + let tempFilter = [], + tempSortDir = ''; + + if (visible) { + tempFilter.push(`visible = "${visible}"`); + } + + if (sortDir) { + tempSortDir = `-created`; + } + + if (name) { + tempFilter.push(`name ~ "%${name}%"`); + } + + if (type) { + tempFilter.push(`type ~ "%${type}%"`); + } + + let preFinalListOption = {}; + if (tempFilter.length > 0) { + preFinalListOption = { filter: tempFilter.join(' && ') }; + } + if (tempSortDir.length > 0) { + preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; + } + setListOption(preFinalListOption); + // setListOption({ + // filter: tempFilter.join(' && '), + // sort: tempSortDir, + // // + // }); + }, [visible, sortDir, name, type]); + + // return <>helloworld; + if (showLoading) return ; if (showError.show) return ( ); @@ -113,7 +169,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { loading={isLoadingAddPage} onClick={(): void => { setIsLoadingAddPage(true); - router.push(paths.dashboard.lp_categories.create); + router.push(paths.dashboard.lp_questions.create); }} startIcon={} variant="contained" @@ -122,22 +178,22 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { - + - - - - + +
{JSON.stringify(f, null, 2)}
); } // Sorting and filtering has to be done on the server. -function applySort(row: LpCategory[], sortDir: 'asc' | 'desc' | undefined): LpCategory[] { +function applySort(row: LpQuestion[], sortDir: 'asc' | 'desc' | undefined): LpQuestion[] { return row.sort((a, b) => { if (sortDir === 'asc') { return a.createdAt.getTime() - b.createdAt.getTime(); @@ -163,7 +220,7 @@ function applySort(row: LpCategory[], sortDir: 'asc' | 'desc' | undefined): LpCa }); } -function applyFilters(row: LpCategory[], { email, phone, status, name, visible }: Filters): LpCategory[] { +function applyFilters(row: LpQuestion[], { email, phone, status, name, visible }: Filters): LpQuestion[] { return row.filter((item) => { if (email) { if (!item.email?.toLowerCase().includes(email.toLowerCase())) { diff --git a/002_source/cms/src/components/dashboard/error/index.tsx b/002_source/cms/src/components/dashboard/error/index.tsx index 46ba2e0..75b2ee2 100644 --- a/002_source/cms/src/components/dashboard/error/index.tsx +++ b/002_source/cms/src/components/dashboard/error/index.tsx @@ -39,11 +39,13 @@ function ErrorDetails({ details }: { details: string }): React.JSX.Element { -
-            
-              {details}
-            
-          
+ + {details} +
@@ -72,7 +74,10 @@ function ErrorDisplay({ message, code, details, severity = 'error' }: PropsError > {code ? `Error ${code}` : 'Error'} - + {formattedMessage} diff --git a/002_source/cms/src/components/dashboard/lp/questions/_PROMPT.MD b/002_source/cms/src/components/dashboard/lp/questions/_PROMPT.MD index 93ce5a6..369cdca 100644 --- a/002_source/cms/src/components/dashboard/lp/questions/_PROMPT.MD +++ b/002_source/cms/src/components/dashboard/lp/questions/_PROMPT.MD @@ -1 +1,21 @@ please review and add translations, e.g. `{t('[word]')}` + +--- + +please help to review the `tsx` file in this folder +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/questions` + +it was clone from +`category`/`categories`, `lp_category`/`lp_categories` +please help to modify to `question`/`questions`, `lp_question`/`lp_questions` + +please also help to modify the name of +`variables`, `constants`, `functions`, `classes`, components's name, paths + +the db fields structures are the same + +do not move the files +do not create directories +keep current folder structure is important + +thanks diff --git a/002_source/cms/src/components/dashboard/lp/questions/_constants.ts b/002_source/cms/src/components/dashboard/lp/questions/_constants.ts index 885067e..2173368 100644 --- a/002_source/cms/src/components/dashboard/lp/questions/_constants.ts +++ b/002_source/cms/src/components/dashboard/lp/questions/_constants.ts @@ -1,11 +1,11 @@ import { dayjs } from '@/lib/dayjs'; -import { CreateFormProps, LpCategory } from './type'; +import { CreateFormProps, LpQuestion } from './type'; -export const defaultLpCategory: LpCategory = { +export const defaultLpQuestion: LpQuestion = { isEmpty: false, id: 'default-id', - cat_name: 'default-category-name', + q_name: 'default-question-name', cat_image_url: undefined, cat_image: undefined, pos: 0, @@ -38,7 +38,7 @@ export const defaultLpCategory: LpCategory = { // imageUrl: '', // }; -export const emptyLpCategory: LpCategory = { - ...defaultLpCategory, +export const emptyLpQuestion: LpQuestion = { + ...defaultLpQuestion, isEmpty: true, }; diff --git a/002_source/cms/src/components/dashboard/lp_questions/lp-category-create-form.tsx b/002_source/cms/src/components/dashboard/lp/questions/lp-question-create-form.tsx similarity index 98% rename from 002_source/cms/src/components/dashboard/lp_questions/lp-category-create-form.tsx rename to 002_source/cms/src/components/dashboard/lp/questions/lp-question-create-form.tsx index ba46ac7..40aca33 100644 --- a/002_source/cms/src/components/dashboard/lp_questions/lp-category-create-form.tsx +++ b/002_source/cms/src/components/dashboard/lp/questions/lp-question-create-form.tsx @@ -37,8 +37,8 @@ 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(), + q_name: zod.string().min(1, 'name-is-required').max(255), + q_image: zod.array(zod.any()).optional(), pos: zod.number().min(1, 'position is required').max(99), init_answer: zod.string().optional(), visible: zod.string(), @@ -66,7 +66,7 @@ export const defaultValues = { description: '', } satisfies Values; -export function LpCategoryCreateForm(): React.JSX.Element { +export function LpQuestionCreateForm(): React.JSX.Element { const router = useRouter(); const { t } = useTranslation(['lp_categories']); diff --git a/002_source/cms/src/components/dashboard/lp_questions/lp-category-edit-form.tsx b/002_source/cms/src/components/dashboard/lp/questions/lp-question-edit-form.tsx similarity index 98% rename from 002_source/cms/src/components/dashboard/lp_questions/lp-category-edit-form.tsx rename to 002_source/cms/src/components/dashboard/lp/questions/lp-question-edit-form.tsx index ba4020d..0f715c1 100644 --- a/002_source/cms/src/components/dashboard/lp_questions/lp-category-edit-form.tsx +++ b/002_source/cms/src/components/dashboard/lp/questions/lp-question-edit-form.tsx @@ -44,10 +44,10 @@ import type { EditFormProps } from './type'; // TODO: review this const schema = zod.object({ - cat_name: zod.string().min(1, 'name-is-required').max(255), + q_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(), + q_image: zod.union([zod.array(zod.any()), zod.string()]).optional(), // position pos: zod.number().min(1, 'position is required').max(99), @@ -88,7 +88,7 @@ const defaultValues = { description: '', } satisfies Values; -export function LpCategoryEditForm(): React.JSX.Element { +export function LpQuestionEditForm(): React.JSX.Element { const router = useRouter(); const { t } = useTranslation(['lp_categories']); diff --git a/002_source/cms/src/components/dashboard/lp/questions/lp-questions-filters.tsx b/002_source/cms/src/components/dashboard/lp/questions/lp-questions-filters.tsx new file mode 100644 index 0000000..d5b5486 --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp/questions/lp-questions-filters.tsx @@ -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 { useLpQuestionsSelection } from './lp-questions-selection-context'; +import { LpQuestion } from './type'; + +export interface Filters { + email?: string; + phone?: string; + status?: string; + name?: string; + visible?: string; + type?: string; +} + +export type SortDir = 'asc' | 'desc'; + +export interface LpQuestionsFiltersProps { + filters?: Filters; + sortDir?: SortDir; + fullData: LpQuestion[]; +} + +export function LpQuestionsFilters({ + filters = {}, + sortDir = 'desc', + fullData, +}: LpQuestionsFiltersProps): React.JSX.Element { + const { t } = useTranslation(); + const { email, phone, status, name, visible, type } = filters; + + const [totalCount, setTotalCount] = React.useState(0); + const [visibleCount, setVisibleCount] = React.useState(0); + const [hiddenCount, setHiddenCount] = React.useState(0); + + const router = useRouter(); + + const selection = useLpQuestionsSelection(); + + function getVisible(): number { + return fullData.reduce((count, item: LpQuestion) => { + return item.visible === 'visible' ? count + 1 : count; + }, 0); + } + + function getHidden(): number { + return fullData.reduce((count, item: LpQuestion) => { + return item.visible === 'hidden' ? count + 1 : count; + }, 0); + } + + // The tabs should be generated using API data. + const tabs = [ + { label: t('All'), value: '', count: totalCount }, + // { label: 'Active', value: 'active', count: 3 }, + // { label: 'Pending', value: 'pending', count: 1 }, + // { label: 'Blocked', value: 'blocked', count: 1 }, + { label: t('visible'), value: 'visible', count: visibleCount }, + { label: t('hidden'), value: 'hidden', count: hiddenCount }, + ] as const; + + const updateSearchParams = React.useCallback( + (newFilters: Filters, newSortDir: SortDir): void => { + const searchParams = new URLSearchParams(); + + if (newSortDir === 'asc') { + searchParams.set('sortDir', newSortDir); + } + + if (newFilters.status) { + searchParams.set('status', newFilters.status); + } + + if (newFilters.email) { + searchParams.set('email', newFilters.email); + } + + if (newFilters.phone) { + searchParams.set('phone', newFilters.phone); + } + + if (newFilters.name) { + searchParams.set('name', newFilters.name); + } + + if (newFilters.type) { + searchParams.set('type', newFilters.type); + } + + if (newFilters.visible) { + searchParams.set('visible', newFilters.visible); + } + + // NOTE: modify according to COLLECTION + router.push(`${paths.dashboard.lp_questions.list}?${searchParams.toString()}`); + }, + [router] + ); + + const handleClearFilters = React.useCallback(() => { + updateSearchParams({}, sortDir); + }, [updateSearchParams, sortDir]); + + const handleStatusChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, status: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleVisibleChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, visible: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleNameChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, name: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleTypeChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, type: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleEmailChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, email: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handlePhoneChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, phone: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleSortChange = React.useCallback( + (event: SelectChangeEvent) => { + updateSearchParams(filters, event.target.value as SortDir); + }, + [updateSearchParams, filters] + ); + + React.useEffect(() => { + const fetchCount = async (): Promise => { + 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 ( +
+ + {tabs.map((tab) => ( + + } + iconPosition="end" + key={tab.value} + label={tab.label} + sx={{ minHeight: 'auto' }} + tabIndex={0} + value={tab.value} + /> + ))} + + + + + { + handleNameChange(value as string); + }} + onFilterDelete={() => { + handleNameChange(); + }} + popover={} + value={name} + /> + + { + handleTypeChange(value as string); + }} + onFilterDelete={() => { + handleTypeChange(); + }} + popover={} + value={type} + /> + + {hasFilters ? : null} + + {selection.selectedAny ? ( + + + {selection.selected.size} {t('selected')} + + + + ) : null} + + +
+ ); +} + +function TypeFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function NameFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function EmailFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function PhoneFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} diff --git a/002_source/cms/src/components/dashboard/lp/questions/lp-questions-pagination.tsx b/002_source/cms/src/components/dashboard/lp/questions/lp-questions-pagination.tsx new file mode 100644 index 0000000..5334248 --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp/questions/lp-questions-pagination.tsx @@ -0,0 +1,49 @@ +'use client'; + +import * as React from 'react'; +import TablePagination from '@mui/material/TablePagination'; + +function noop(): void { + return undefined; +} + +interface LessonQuestionsPaginationProps { + count: number; + page: number; + // + setPage: (page: number) => void; + setRowsPerPage: (page: number) => void; + rowsPerPage: number; +} + +export function LpQuestionsPagination({ + 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) => { + setRowsPerPage(parseInt(event.target.value)); + // console.log(parseInt(event.target.value)); + }; + + return ( + + ); +} diff --git a/002_source/cms/src/components/dashboard/lp/questions/lp-questions-selection-context.tsx b/002_source/cms/src/components/dashboard/lp/questions/lp-questions-selection-context.tsx new file mode 100644 index 0000000..fe47149 --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp/questions/lp-questions-selection-context.tsx @@ -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 { LpQuestion } from './type'; + +function noop(): void { + return undefined; +} + +export interface LpQuestionsSelectionContextValue extends Selection {} + +export const LpQuestionsSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface LpQuestionsSelectionProviderProps { + children: React.ReactNode; + lessonCategories: LpQuestion[]; +} + +export function LpQuestionsSelectionProvider({ + children, + lessonCategories = [], +}: LpCategoriesSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]); + const selection = useSelection(customerIds); + + return ( + {children} + ); +} + +export function useLpQuestionsSelection(): LpQuestionsSelectionContextValue { + return React.useContext(LpCategoriesSelectionContext); +} diff --git a/002_source/cms/src/components/dashboard/lp_questions/lp-categories-table.tsx b/002_source/cms/src/components/dashboard/lp/questions/lp-questions-table.tsx similarity index 95% rename from 002_source/cms/src/components/dashboard/lp_questions/lp-categories-table.tsx rename to 002_source/cms/src/components/dashboard/lp/questions/lp-questions-table.tsx index 26034f1..8369423 100644 --- a/002_source/cms/src/components/dashboard/lp_questions/lp-categories-table.tsx +++ b/002_source/cms/src/components/dashboard/lp/questions/lp-questions-table.tsx @@ -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 { useLpCategoriesSelection } from './lp-categories-selection-context'; -import type { LpCategory } from './type'; +import { useLpQuestionsSelection } from './lp-questions-selection-context'; +import type { LpQuestion } from './type'; -function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { +function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { return [ { formatter: (row): React.JSX.Element => ( @@ -186,12 +186,12 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef void; } -export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element { +export function LpQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps): React.JSX.Element { const { t } = useTranslation(['lp_categories']); const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpCategoriesSelection(); diff --git a/002_source/cms/src/components/dashboard/lp/questions/type.d.ts b/002_source/cms/src/components/dashboard/lp/questions/type.d.ts index c66a11f..3e6e084 100644 --- a/002_source/cms/src/components/dashboard/lp/questions/type.d.ts +++ b/002_source/cms/src/components/dashboard/lp/questions/type.d.ts @@ -1,12 +1,12 @@ -export interface LpCategory { +export interface LpQuestion { isEmpty?: boolean; // id: string; collectionId: string; // - cat_name: string; - cat_image_url?: string; - cat_image?: string; + q_name: string; + q_image_url?: string; + q_image?: string; pos: number; visible: string; lesson_id: string; @@ -25,8 +25,8 @@ export interface LpCategory { } export interface CreateFormProps { - cat_name: string; - cat_image: File[] | null; + q_name: string; + q_image: File[] | null; pos: number; init_answer?: string; visible: string; @@ -43,8 +43,8 @@ export interface CreateFormProps { } export interface EditFormProps { - cat_name: string; - cat_image: File[] | null; + q_name: string; + q_image: File[] | null; pos: number; init_answer: any; visible: string; diff --git a/002_source/cms/src/components/dashboard/lp_questions.plan/_PROMPT.MD b/002_source/cms/src/components/dashboard/lp_questions.plan/_PROMPT.MD new file mode 100644 index 0000000..93ce5a6 --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp_questions.plan/_PROMPT.MD @@ -0,0 +1 @@ +please review and add translations, e.g. `{t('[word]')}` diff --git a/002_source/cms/src/components/dashboard/lp_questions.plan/_constants.ts b/002_source/cms/src/components/dashboard/lp_questions.plan/_constants.ts new file mode 100644 index 0000000..885067e --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp_questions.plan/_constants.ts @@ -0,0 +1,44 @@ +import { dayjs } from '@/lib/dayjs'; + +import { CreateFormProps, LpCategory } from './type'; + +export const defaultLpCategory: LpCategory = { + isEmpty: false, + id: 'default-id', + cat_name: 'default-category-name', + cat_image_url: undefined, + cat_image: undefined, + pos: 0, + visible: 'hidden', + lesson_id: 'default-lesson-id', + description: 'default-description', + remarks: 'default-remarks', + slug: '', + init_answer: {}, + // from pocketbase + collectionId: '0000000000', + createdAt: dayjs('2099-01-01').toDate(), + // + name: '', + avatar: '', + email: '', + phone: '', + quota: 0, + status: 'NA', +}; + +// export const LpCategoryCreateFormDefault: CreateFormProps = { +// name: '', +// type: '', +// pos: 1, +// visible: 'visible', +// description: '', +// isActive: true, +// order: 1, +// imageUrl: '', +// }; + +export const emptyLpCategory: LpCategory = { + ...defaultLpCategory, + isEmpty: true, +}; diff --git a/002_source/cms/src/components/dashboard/lp_questions.plan/confirm-delete-modal.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/confirm-delete-modal.tsx new file mode 100644 index 0000000..32daa09 --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp_questions.plan/confirm-delete-modal.tsx @@ -0,0 +1,122 @@ +'use client'; + +import * as React from 'react'; +import deleteQuizLPCategories from '@/db/QuizListenings/Delete'; +import { LoadingButton } from '@mui/lab'; +import { Button, Container, Modal, Paper } from '@mui/material'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note'; +import { useTranslation } from 'react-i18next'; + +import { logger } from '@/lib/default-logger'; +import { toast } from '@/components/core/toaster'; + +export default function ConfirmDeleteModal({ + open, + setOpen, + idToDelete, + reloadRows, +}: { + open: boolean; + setOpen: (b: boolean) => void; + idToDelete: string; + reloadRows: () => void; +}): React.JSX.Element { + const { t } = useTranslation(); + + // const handleClose = () => setOpen(false); + function handleClose(): void { + setOpen(false); + } + + const [isDeleteing, setIsDeleteing] = React.useState(false); + const style = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + }; + + function handleUserConfirmDelete(): void { + if (idToDelete) { + setIsDeleteing(true); + deleteQuizLPCategories(idToDelete) + .then(() => { + reloadRows(); + handleClose(); + toast(t('delete.success')); + }) + .catch((err) => { + // console.error(err) + logger.error(err); + toast(t('delete.error')); + }) + .finally(() => { + setIsDeleteing(false); + }); + } + } + + return ( +
+ + + + + + + + + + + {t('Delete Lesson Type ?')} + + {t('Are you sure you want to delete lesson type ?')} + + + + + { + handleUserConfirmDelete(); + }} + loading={isDeleteing} + > + {t('Delete')} + + + + + + + + +
+ ); +} diff --git a/002_source/cms/src/components/dashboard/lp_questions.plan/lp-question-create-form.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-question-create-form.tsx new file mode 100644 index 0000000..d7548cc --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-question-create-form.tsx @@ -0,0 +1,419 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +import { Avatar, Divider, MenuItem, Select } from '@mui/material'; +// import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// import axios from 'axios'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import isDevelopment from '@/lib/check-is-development'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; + +import type { CreateFormProps } from './type'; + +const schema = zod.object({ + question_name: zod.string().min(1, 'name-is-required').max(255), + question_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; + +export const defaultValues = { + question_name: '', + question_image: undefined, + pos: 1, + init_answer: '', + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function LpQuestionCreateForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['lp_questions']); + + const [isCreating, setIsCreating] = React.useState(false); + + const { + control, + handleSubmit, + formState: { errors }, + setValue, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + setIsCreating(true); + + const payload: CreateFormProps = { + question_name: values.question_name, + question_image: values.avatar ? [await base64ToFile(values.avatar)] : null, + pos: values.pos, + init_answer: values.init_answer, + visible: values.visible, + slug: values.slug, + remarks: values.remarks, + description: values.description, + // + // TODO: remove me + type: 'type tet', + isActive: true, + order: 1, + }; + + try { + const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).create(payload); + + logger.debug(result); + toast.success(t('create.success')); + router.push(paths.dashboard.lp_questions.list); + } catch (error) { + logger.error(error); + toast.error(t('create.failed')); + } finally { + setIsCreating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(null); + const avatar = watch('avatar'); + + const handleAvatarChange = React.useCallback( + async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + + if (file) { + const url = await fileToBase64(file); + setValue('avatar', url); + } + }, + [setValue] + ); + + return ( +
+ + + } + spacing={4} + > + + {t('create.basic-info')} + + + + + + + + + + {t('create.avatar')} + {t('create.avatarRequirements')} + + + + + + + ( + + {t('create.question_name')} + + {errors.question_name ? {errors.question_name.message} : null} + + )} + /> + + {/* */} + + ( + + {t('create.pos')} + { + field.onChange(parseInt(e.target.value)); + }} + type="number" + /> + {errors.pos ? {errors.pos.message} : null} + + )} + /> + + {/* */} + + ( + + {t('create.slug')} + + {errors.slug ? {errors.slug.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.visible')} + + + {errors.visible ? {errors.visible.message} : null} + + )} + /> + + {/* */} + + ( + + {t('create.init_answer')} + + {errors.init_answer ? {errors.init_answer.message} : null} + + )} + /> + + + + {/* */} + + {t('create.detail-information')} + + + { + return ( + + + {t('create.description')} + + + { + field.onChange({ target: { value: editor.getHTML() } }); + }} + placeholder={t('create.description.default')} + /> + + + ); + }} + /> + + + ( + + + {t('create.remarks')} + + + { + field.onChange({ target: { value: editor.getText() } }); + }} + hideToolbar + placeholder={t('create.remarks.default')} + /> + + + )} + /> + + + + {/* */} + + + + + + + {t('create.createButton')} + + + + +
{JSON.stringify({ errors }, null, 2)}
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/lp_questions.plan/lp-question-edit-form.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-question-edit-form.tsx new file mode 100644 index 0000000..61f7aec --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-question-edit-form.tsx @@ -0,0 +1,499 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +// +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({ + question_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 + question_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; + +const defaultValues = { + question_name: '', + question_image: undefined, + pos: 1, + init_answer: JSON.stringify({}), + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function LpQuestionEditForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['lp_questions']); + + const { cat_id: catId } = useParams<{ cat_id: string }>(); + // + const [isUpdating, setIsUpdating] = React.useState(false); + const [showLoading, setShowLoading] = React.useState(false); + // + const [showError, setShowError] = React.useState({ show: false, detail: '' }); + + const { + control, + handleSubmit, + formState: { errors }, + setValue, + reset, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + setIsUpdating(true); + + const tempUpdate: EditFormProps = { + question_name: values.question_name, + question_image: values.avatar ? [await base64ToFile(values.avatar)] : null, + pos: values.pos, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + init_answer: JSON.parse(values.init_answer || '{}'), + + visible: values.visible, + slug: values.slug || 'not-defined', + remarks: values.remarks, + description: values.description, + // + // TODO: remove below + type: '', + }; + + try { + const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).update(catId, tempUpdate); + logger.debug(result); + toast.success(t('edit.success')); + router.push(paths.dashboard.lp_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(null); + const avatar = watch('avatar'); + + const handleAvatarChange = React.useCallback( + async (event: React.ChangeEvent) => { + 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(''); + const [textRemarks, setTextRemarks] = React.useState(''); + + // load existing data when user arrive + const loadExistingData = React.useCallback( + async (id: string) => { + setShowLoading(true); + + try { + const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).getOne(id); + + reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); + setTextDescription(result.description); + setTextRemarks(result.remarks); + + if (result.question_image !== '') { + const fetchResult = await fetch( + `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.question_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 ; + if (showError.show) + return ( + + ); + + return ( +
+ + + } + spacing={4} + > + + {t('edit.basic-info')} + + + + + + + + + + {t('edit.avatar')} + {t('edit.avatarRequirements')} + + + + + + + ( + + {t('edit.question_name')} + + {errors.question_name ? {errors.question_name.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.pos')} + { + field.onChange(parseInt(e.target.value)); + }} + type="number" + /> + {errors.pos ? {errors.pos.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.slug')} + + {errors.slug ? {errors.slug.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.visible')} + + + {errors.visible ? {errors.visible.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.init_answer')} + + {errors.init_answer ? {errors.init_answer.message} : null} + + )} + /> + + + + {/* */} + + {t('edit.detail-information')} + + + { + return ( + + + {t('edit.description')} + + + { + field.onChange({ target: { value: editor.getHTML() } }); + }} + placeholder={t('edit.description.default')} + /> + + + ); + }} + /> + + + ( + + + {t('edit.remarks')} + + + { + field.onChange({ target: { value: editor.getText() } }); + }} + hideToolbar + placeholder={t('edit.remarks.default')} + /> + + + )} + /> + + + + {/* */} + + + + + + + {t('edit.updateButton')} + + + +
+ ); +} diff --git a/002_source/cms/src/components/dashboard/lp/questions/lp-categories-filters.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-filters.tsx similarity index 99% rename from 002_source/cms/src/components/dashboard/lp/questions/lp-categories-filters.tsx rename to 002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-filters.tsx index 7573cc6..168ef12 100644 --- a/002_source/cms/src/components/dashboard/lp/questions/lp-categories-filters.tsx +++ b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-filters.tsx @@ -23,7 +23,7 @@ import { paths } from '@/paths'; import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; import { Option } from '@/components/core/option'; -import { useLpCategoriesSelection } from './lp-categories-selection-context'; +import { useLpCategoriesSelection } from './lp-questions-selection-context'; import { LpCategory } from './type'; export interface Filters { diff --git a/002_source/cms/src/components/dashboard/lp/questions/lp-categories-pagination.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-pagination.tsx similarity index 100% rename from 002_source/cms/src/components/dashboard/lp/questions/lp-categories-pagination.tsx rename to 002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-pagination.tsx diff --git a/002_source/cms/src/components/dashboard/lp/questions/lp-categories-selection-context.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-selection-context.tsx similarity index 100% rename from 002_source/cms/src/components/dashboard/lp/questions/lp-categories-selection-context.tsx rename to 002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-selection-context.tsx diff --git a/002_source/cms/src/components/dashboard/lp/questions/lp-categories-table.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-table.tsx similarity index 98% rename from 002_source/cms/src/components/dashboard/lp/questions/lp-categories-table.tsx rename to 002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-table.tsx index 26034f1..5ef6506 100644 --- a/002_source/cms/src/components/dashboard/lp/questions/lp-categories-table.tsx +++ b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-table.tsx @@ -25,7 +25,7 @@ import { DataTable } from '@/components/core/data-table'; import type { ColumnDef } from '@/components/core/data-table'; import ConfirmDeleteModal from './confirm-delete-modal'; -import { useLpCategoriesSelection } from './lp-categories-selection-context'; +import { useLpCategoriesSelection } from './lp-questions-selection-context'; import type { LpCategory } from './type'; function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { diff --git a/002_source/cms/src/components/dashboard/lp_questions.plan/notifications.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/notifications.tsx new file mode 100644 index 0000000..a6c16bd --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp_questions.plan/notifications.tsx @@ -0,0 +1,101 @@ +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; + +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; +import { Option } from '@/components/core/option'; + +export interface Notification { + id: string; + type: string; + status: 'delivered' | 'pending' | 'failed'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {row.type} + + ), + name: 'Type', + width: '300px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + delivered: { label: 'Delivered', color: 'success' }, + pending: { label: 'Pending', color: 'warning' }, + failed: { label: 'Failed', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface NotificationsProps { + notifications: Notification[]; +} + +export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { + return ( + + + + + } + title="Notifications" + /> + + + + +
+ +
+
+ + + columns={columns} rows={notifications} /> + + +
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/lp_questions.plan/payments.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/payments.tsx new file mode 100644 index 0000000..0420d32 --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp_questions.plan/payments.tsx @@ -0,0 +1,138 @@ +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple'; + +import { dayjs } from '@/lib/dayjs'; +import type { ColumnDef } from '@/components/core/data-table'; +import { DataTable } from '@/components/core/data-table'; + +export interface Payment { + currency: string; + amount: number; + invoiceId: string; + status: 'pending' | 'completed' | 'canceled' | 'refunded'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)} + + ), + name: 'Amount', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + pending: { label: 'Pending', color: 'warning' }, + completed: { label: 'Completed', color: 'success' }, + canceled: { label: 'Canceled', color: 'error' }, + refunded: { label: 'Refunded', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + return {row.invoiceId}; + }, + name: 'Invoice ID', + width: '150px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface PaymentsProps { + ordersValue: number; + payments: Payment[]; + refundsValue: number; + totalOrders: number; +} + +export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { + return ( + + }> + Create Payment + + } + avatar={ + + + + } + title="Payments" + /> + + + + } + spacing={3} + sx={{ justifyContent: 'space-between', p: 2 }} + > +
+ + Total orders + + {new Intl.NumberFormat('en-US').format(totalOrders)} +
+
+ + Orders value + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} + +
+
+ + Refunds + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} + +
+
+
+ + + columns={columns} rows={payments} /> + + +
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/lp_questions.plan/shipping-address.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/shipping-address.tsx new file mode 100644 index 0000000..8793e5c --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp_questions.plan/shipping-address.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; + +export interface Address { + id: string; + country: string; + state: string; + city: string; + zipCode: string; + street: string; + primary?: boolean; +} + +export interface ShippingAddressProps { + address: Address; +} + +export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement { + return ( + + + + + {address.street}, +
+ {address.city}, {address.state}, {address.country}, +
+ {address.zipCode} +
+ + {address.primary ? : } + + +
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/lp_questions.plan/type.d.ts b/002_source/cms/src/components/dashboard/lp_questions.plan/type.d.ts new file mode 100644 index 0000000..1b9fc87 --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp_questions.plan/type.d.ts @@ -0,0 +1,61 @@ +export interface LpQuestion { + isEmpty?: boolean; + // + id: string; + collectionId: string; + // + question_name: string; + question_image_url?: string; + question_image?: string; + pos: number; + visible: string; + lesson_id: string; + description: string; + remarks: string; + slug: string; + init_answer: any; + // + name: string; + avatar: string; + email: string; + phone: string; + quota: number; + status: 'pending' | 'active' | 'blocked' | 'NA'; + createdAt: Date; +} + +export interface CreateFormProps { + question_name: string; + question_image: File[] | null; + pos: number; + init_answer?: string; + visible: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: to remove + type: string; + isActive: boolean; + order: number; + name?: string; + imageUrl?: string; +} + +export interface EditFormProps { + question_name: string; + question_image: File[] | null; + pos: number; + init_answer: any; + visible: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: remove below + type: string; +} + +export interface Helloworld { + helloworld: string; +} diff --git a/002_source/cms/src/components/dashboard/lp_questions/_constants.ts b/002_source/cms/src/components/dashboard/lp_questions/_constants.ts index 885067e..dafb8fd 100644 --- a/002_source/cms/src/components/dashboard/lp_questions/_constants.ts +++ b/002_source/cms/src/components/dashboard/lp_questions/_constants.ts @@ -1,8 +1,8 @@ import { dayjs } from '@/lib/dayjs'; -import { CreateFormProps, LpCategory } from './type'; +import { CreateFormProps, LpQuestion } from './type'; -export const defaultLpCategory: LpCategory = { +export const defaultLpQuestion: LpQuestion = { 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: LpQuestion = { + ...defaultLpQuestion, isEmpty: true, }; diff --git a/002_source/cms/src/components/dashboard/lp_questions/confirm-delete-modal.tsx b/002_source/cms/src/components/dashboard/lp_questions/confirm-delete-modal.tsx index 4d5b267..e03ca52 100644 --- a/002_source/cms/src/components/dashboard/lp_questions/confirm-delete-modal.tsx +++ b/002_source/cms/src/components/dashboard/lp_questions/confirm-delete-modal.tsx @@ -1,9 +1,7 @@ 'use client'; import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import { COL_LESSON_TYPES } from '@/constants'; -import deleteQuizLPCategories from '@/db/QuizListenings/Delete'; +import deleteQuizLPQuestions from '@/db/QuizLPQuestions/Delete'; import { LoadingButton } from '@mui/lab'; import { Button, Container, Modal, Paper } from '@mui/material'; import Avatar from '@mui/material/Avatar'; @@ -11,7 +9,6 @@ import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note'; -import PocketBase from 'pocketbase'; import { useTranslation } from 'react-i18next'; import { logger } from '@/lib/default-logger'; @@ -46,7 +43,7 @@ export default function ConfirmDeleteModal({ function handleUserConfirmDelete(): void { if (idToDelete) { setIsDeleteing(true); - deleteQuizLPCategories(idToDelete) + deleteQuizLPQuestions(idToDelete) .then(() => { reloadRows(); handleClose(); diff --git a/002_source/cms/src/components/dashboard/lp/questions/lp-category-create-form.tsx b/002_source/cms/src/components/dashboard/lp_questions/lp-question-create-form.tsx similarity index 97% rename from 002_source/cms/src/components/dashboard/lp/questions/lp-category-create-form.tsx rename to 002_source/cms/src/components/dashboard/lp_questions/lp-question-create-form.tsx index ba46ac7..ddafd5b 100644 --- a/002_source/cms/src/components/dashboard/lp/questions/lp-category-create-form.tsx +++ b/002_source/cms/src/components/dashboard/lp_questions/lp-question-create-form.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import RouterLink from 'next/link'; import { useRouter } from 'next/navigation'; -import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; import { zodResolver } from '@hookform/resolvers/zod'; import { LoadingButton } from '@mui/lab'; import { Avatar, Divider, MenuItem, Select } from '@mui/material'; @@ -66,9 +66,9 @@ export const defaultValues = { description: '', } satisfies Values; -export function LpCategoryCreateForm(): React.JSX.Element { +export function LpQuestionCreateForm(): React.JSX.Element { const router = useRouter(); - const { t } = useTranslation(['lp_categories']); + const { t } = useTranslation(['lp_questions']); const [isCreating, setIsCreating] = React.useState(false); @@ -101,11 +101,11 @@ export function LpCategoryCreateForm(): React.JSX.Element { }; try { - const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).create(payload); + const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).create(payload); logger.debug(result); toast.success(t('create.success')); - router.push(paths.dashboard.lp_categories.list); + router.push(paths.dashboard.lp_questions.list); } catch (error) { logger.error(error); toast.error(t('create.failed')); @@ -396,7 +396,7 @@ export function LpCategoryCreateForm(): React.JSX.Element { diff --git a/002_source/cms/src/components/dashboard/lp/questions/lp-category-edit-form.tsx b/002_source/cms/src/components/dashboard/lp_questions/lp-question-edit-form.tsx similarity index 97% rename from 002_source/cms/src/components/dashboard/lp/questions/lp-category-edit-form.tsx rename to 002_source/cms/src/components/dashboard/lp_questions/lp-question-edit-form.tsx index ba4020d..f4eb2ef 100644 --- a/002_source/cms/src/components/dashboard/lp/questions/lp-category-edit-form.tsx +++ b/002_source/cms/src/components/dashboard/lp_questions/lp-question-edit-form.tsx @@ -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_LP_QUESTIONS } from '@/constants'; import { zodResolver } from '@hookform/resolvers/zod'; import { LoadingButton } from '@mui/lab'; // @@ -88,9 +88,9 @@ const defaultValues = { description: '', } satisfies Values; -export function LpCategoryEditForm(): React.JSX.Element { +export function LpQuestionEditForm(): React.JSX.Element { const router = useRouter(); - const { t } = useTranslation(['lp_categories']); + const { t } = useTranslation(['lp_questions']); const { cat_id: catId } = useParams<{ cat_id: string }>(); // @@ -129,10 +129,10 @@ export function LpCategoryEditForm(): React.JSX.Element { }; try { - const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate); + const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).update(catId, tempUpdate); logger.debug(result); toast.success(t('edit.success')); - router.push(paths.dashboard.lp_categories.list); + router.push(paths.dashboard.lp_questions.list); } catch (error) { logger.error(error); toast.error(t('update.failed')); @@ -171,7 +171,7 @@ export function LpCategoryEditForm(): React.JSX.Element { setShowLoading(true); try { - const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); + const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).getOne(id); reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); setTextDescription(result.description); @@ -480,7 +480,7 @@ export function LpCategoryEditForm(): React.JSX.Element { diff --git a/002_source/cms/src/components/dashboard/lp_questions/lp-categories-filters.tsx b/002_source/cms/src/components/dashboard/lp_questions/lp-questions-filters.tsx similarity index 96% rename from 002_source/cms/src/components/dashboard/lp_questions/lp-categories-filters.tsx rename to 002_source/cms/src/components/dashboard/lp_questions/lp-questions-filters.tsx index 7573cc6..36c4b0b 100644 --- a/002_source/cms/src/components/dashboard/lp_questions/lp-categories-filters.tsx +++ b/002_source/cms/src/components/dashboard/lp_questions/lp-questions-filters.tsx @@ -23,8 +23,8 @@ import { paths } from '@/paths'; import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; import { Option } from '@/components/core/option'; -import { useLpCategoriesSelection } from './lp-categories-selection-context'; -import { LpCategory } from './type'; +import { useLpCategoriesSelection } from './lp-questions-selection-context'; +import { LpQuestion } from './type'; export interface Filters { email?: string; @@ -40,10 +40,10 @@ export type SortDir = 'asc' | 'desc'; export interface LpCategoriesFiltersProps { filters?: Filters; sortDir?: SortDir; - fullData: LpCategory[]; + fullData: LpQuestion[]; } -export function LpCategoriesFilters({ +export function LpQuestionsFilters({ filters = {}, sortDir = 'desc', fullData, @@ -60,13 +60,13 @@ export function LpCategoriesFilters({ const selection = useLpCategoriesSelection(); function getVisible(): number { - return fullData.reduce((count, item: LpCategory) => { + return fullData.reduce((count, item: LpQuestion) => { return item.visible === 'visible' ? count + 1 : count; }, 0); } function getHidden(): number { - return fullData.reduce((count, item: LpCategory) => { + return fullData.reduce((count, item: LpQuestion) => { return item.visible === 'hidden' ? count + 1 : count; }, 0); } @@ -114,7 +114,7 @@ export function LpCategoriesFilters({ } // NOTE: modify according to COLLECTION - router.push(`${paths.dashboard.lp_categories.list}?${searchParams.toString()}`); + router.push(`${paths.dashboard.lp_questions.list}?${searchParams.toString()}`); }, [router] ); diff --git a/002_source/cms/src/components/dashboard/lp_questions/lp-categories-pagination.tsx b/002_source/cms/src/components/dashboard/lp_questions/lp-questions-pagination.tsx similarity index 96% rename from 002_source/cms/src/components/dashboard/lp_questions/lp-categories-pagination.tsx rename to 002_source/cms/src/components/dashboard/lp_questions/lp-questions-pagination.tsx index bf40481..652ed55 100644 --- a/002_source/cms/src/components/dashboard/lp_questions/lp-categories-pagination.tsx +++ b/002_source/cms/src/components/dashboard/lp_questions/lp-questions-pagination.tsx @@ -16,7 +16,7 @@ interface LessonCategoriesPaginationProps { rowsPerPage: number; } -export function LpCategoriesPagination({ +export function LpQuestionsPagination({ count, page, // diff --git a/002_source/cms/src/components/dashboard/lp_questions/lp-categories-selection-context.tsx b/002_source/cms/src/components/dashboard/lp_questions/lp-questions-selection-context.tsx similarity index 88% rename from 002_source/cms/src/components/dashboard/lp_questions/lp-categories-selection-context.tsx rename to 002_source/cms/src/components/dashboard/lp_questions/lp-questions-selection-context.tsx index a6c6502..b4fafda 100644 --- a/002_source/cms/src/components/dashboard/lp_questions/lp-categories-selection-context.tsx +++ b/002_source/cms/src/components/dashboard/lp_questions/lp-questions-selection-context.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { useSelection } from '@/hooks/use-selection'; import type { Selection } from '@/hooks/use-selection'; -import { LpCategory } from './type'; +import { LpQuestion } from './type'; function noop(): void { return undefined; @@ -26,12 +26,12 @@ export const LpCategoriesSelectionContext = React.createContext lessonCategories.map((customer) => customer.id), [lessonCategories]); const selection = useSelection(customerIds); diff --git a/002_source/cms/src/components/dashboard/lp_questions/lp-questions-table.tsx b/002_source/cms/src/components/dashboard/lp_questions/lp-questions-table.tsx new file mode 100644 index 0000000..eab1353 --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp_questions/lp-questions-table.tsx @@ -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 './lp-questions-selection-context'; +import type { LpQuestion } from './type'; + +function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { + return [ + { + formatter: (row): React.JSX.Element => ( + + + + + + {' '} +
+ {row.cat_name} + + slug: {row.cat_name} + +
+
+ +
+ ), + name: 'Name', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} + + + ), + // 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: ( + + ), + }, + blocked: { label: 'Blocked', icon: }, + pending: { + label: 'Pending', + icon: ( + + ), + }, + NA: { + label: 'NA', + icon: ( + + ), + }, + } as const; + const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; + + return ( + + ); + }, + name: 'visible', + width: '150px', + }, + { + formatter(row) { + return dayjs(row.createdAt).format('MMM D, YYYY'); + }, + name: 'Created at', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + + { + handleDeleteClick(row.id); + }} + > + + + + ), + name: 'Actions', + hideName: true, + width: '100px', + align: 'right', + }, + ]; +} + +export interface LessonCategoriesTableProps { + rows: LpQuestion[]; + reloadRows: () => void; +} + +export function LpQuestionsTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element { + const { t } = useTranslation(['lp_questions']); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpCategoriesSelection(); + + const [idToDelete, setIdToDelete] = React.useState(''); + const [open, setOpen] = React.useState(false); + + function handleDeleteClick(testId: string): void { + setOpen(true); + setIdToDelete(testId); + } + + return ( + + + + columns={columns(handleDeleteClick)} + onDeselectAll={deselectAll} + onDeselectOne={(_, row) => { + deselectOne(row.id); + }} + onSelectAll={selectAll} + onSelectOne={(_, row) => { + selectOne(row.id); + }} + rows={rows} + selectable + selected={selected} + /> + {!rows.length ? ( + + + {t('no-lesson-categories-found')} + + + ) : null} + + ); +} diff --git a/002_source/cms/src/components/dashboard/lp_questions/type.d.ts b/002_source/cms/src/components/dashboard/lp_questions/type.d.ts index c66a11f..8975fb4 100644 --- a/002_source/cms/src/components/dashboard/lp_questions/type.d.ts +++ b/002_source/cms/src/components/dashboard/lp_questions/type.d.ts @@ -1,4 +1,4 @@ -export interface LpCategory { +export interface LpQuestion { isEmpty?: boolean; // id: string; diff --git a/002_source/cms/src/db/DB_AI_GUIDELINE.MD b/002_source/cms/src/db/DB_AI_GUIDELINE.MD index c61bf54..f2a9873 100644 --- a/002_source/cms/src/db/DB_AI_GUIDELINE.MD +++ b/002_source/cms/src/db/DB_AI_GUIDELINE.MD @@ -2,27 +2,34 @@ ## 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` -please read `/001_documentation/Requirements/REQ0006/schema.dbml` +- read `/001_documentation/Requirements/REQ0006/schema.dbml` this is file in dbml syntax state the main database -please read `/002_source/cms/src/db/schema.json` +- read `/002_source/cms/src/db/schema.json` this is the file of live pocketbase schema output -please read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts` +- read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts` this is the content of `@/constants` -please look into the md files in folder `/002_source/cms/_AI_GUIDELINE` +- look into the md files in folder `/002_source/cms/_AI_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. -please read, remember and link up the ideas in file stated above, -i will tell you the task afterwards this is now not in debug phase, -so, no need to reply me what you are going on or your insight throught the prompt. +so, no need to reply me what they are going on or their insight throught the prompt. just reply me "OK" when done --- diff --git a/002_source/cms/src/db/QuizLPCategories/Create.tsx b/002_source/cms/src/db/QuizLPCategories/Create.tsx new file mode 100644 index 0000000..999b21a --- /dev/null +++ b/002_source/cms/src/db/QuizLPCategories/Create.tsx @@ -0,0 +1,12 @@ +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface CreateForm { + // TODO: Add QuizLPCategories fields +} + +export default function createQuizLPCategory(data: CreateForm): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).create(data); +} diff --git a/002_source/cms/src/db/QuizLPCategories/Delete.tsx b/002_source/cms/src/db/QuizLPCategories/Delete.tsx new file mode 100644 index 0000000..7199c87 --- /dev/null +++ b/002_source/cms/src/db/QuizLPCategories/Delete.tsx @@ -0,0 +1,7 @@ +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizLPCategories(id: string): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).delete(id); +} diff --git a/002_source/cms/src/db/QuizLPCategories/GetAll.tsx b/002_source/cms/src/db/QuizLPCategories/GetAll.tsx new file mode 100644 index 0000000..63cd429 --- /dev/null +++ b/002_source/cms/src/db/QuizLPCategories/GetAll.tsx @@ -0,0 +1,8 @@ +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizLPCategories(): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getFullList(); +} diff --git a/002_source/cms/src/db/QuizLPCategories/GetAllCount.tsx b/002_source/cms/src/db/QuizLPCategories/GetAllCount.tsx new file mode 100644 index 0000000..e6027ba --- /dev/null +++ b/002_source/cms/src/db/QuizLPCategories/GetAllCount.tsx @@ -0,0 +1,9 @@ +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, {}); + return count; +} diff --git a/002_source/cms/src/db/QuizLPCategories/GetById.tsx b/002_source/cms/src/db/QuizLPCategories/GetById.tsx new file mode 100644 index 0000000..fed5605 --- /dev/null +++ b/002_source/cms/src/db/QuizLPCategories/GetById.tsx @@ -0,0 +1,8 @@ +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizLPCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); +} diff --git a/002_source/cms/src/db/QuizLPCategories/GetHiddenCount.tsx b/002_source/cms/src/db/QuizLPCategories/GetHiddenCount.tsx new file mode 100644 index 0000000..ea9b0d3 --- /dev/null +++ b/002_source/cms/src/db/QuizLPCategories/GetHiddenCount.tsx @@ -0,0 +1,14 @@ +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizLPCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} diff --git a/002_source/cms/src/db/QuizLPCategories/GetVisibleCount.tsx b/002_source/cms/src/db/QuizLPCategories/GetVisibleCount.tsx new file mode 100644 index 0000000..8f6123e --- /dev/null +++ b/002_source/cms/src/db/QuizLPCategories/GetVisibleCount.tsx @@ -0,0 +1,14 @@ +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizLPCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} diff --git a/002_source/cms/src/db/QuizLPCategories/Update.tsx b/002_source/cms/src/db/QuizLPCategories/Update.tsx new file mode 100644 index 0000000..d7982d4 --- /dev/null +++ b/002_source/cms/src/db/QuizLPCategories/Update.tsx @@ -0,0 +1,9 @@ +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp_categories/type'; + +export default function updateQuizLPCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).update(id, data); +} diff --git a/002_source/cms/src/db/QuizLPCategories/_PROMPT.md b/002_source/cms/src/db/QuizLPCategories/_PROMPT.md new file mode 100644 index 0000000..2041c00 --- /dev/null +++ b/002_source/cms/src/db/QuizLPCategories/_PROMPT.md @@ -0,0 +1,18 @@ +please help to review the `tsx` file in this folder +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPCategories` + +it was clone from +`MFCategories` +please help to modify to +`LPCategories` + +please also help to modify the name of +`variables`, `constants`, `functions`, `classes`, components's name, paths + +the db fields structures between them are the same + +do not move the files +do not create directories +keep current folder structure is important + +thanks diff --git a/002_source/cms/src/db/QuizLPQuestions/Create.tsx b/002_source/cms/src/db/QuizLPQuestions/Create.tsx new file mode 100644 index 0000000..a73c53d --- /dev/null +++ b/002_source/cms/src/db/QuizLPQuestions/Create.tsx @@ -0,0 +1,12 @@ +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface CreateForm { + // TODO: Add QuizLPQuestions fields +} + +export default function createQuizLPQuestion(data: CreateForm): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).create(data); +} diff --git a/002_source/cms/src/db/QuizLPQuestions/Delete.tsx b/002_source/cms/src/db/QuizLPQuestions/Delete.tsx new file mode 100644 index 0000000..f2e1176 --- /dev/null +++ b/002_source/cms/src/db/QuizLPQuestions/Delete.tsx @@ -0,0 +1,7 @@ +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizLPQuestions(id: string): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).delete(id); +} diff --git a/002_source/cms/src/db/QuizLPQuestions/GetAll.tsx b/002_source/cms/src/db/QuizLPQuestions/GetAll.tsx new file mode 100644 index 0000000..f6c5b5c --- /dev/null +++ b/002_source/cms/src/db/QuizLPQuestions/GetAll.tsx @@ -0,0 +1,8 @@ +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizLPQuestions(): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).getFullList(); +} diff --git a/002_source/cms/src/db/QuizLPQuestions/GetAllCount.tsx b/002_source/cms/src/db/QuizLPQuestions/GetAllCount.tsx new file mode 100644 index 0000000..b397989 --- /dev/null +++ b/002_source/cms/src/db/QuizLPQuestions/GetAllCount.tsx @@ -0,0 +1,9 @@ +// REQ0006 +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_LP_QUESTIONS).getList(1, 9999, {}); + return count; +} diff --git a/002_source/cms/src/db/QuizLPQuestions/GetById.tsx b/002_source/cms/src/db/QuizLPQuestions/GetById.tsx new file mode 100644 index 0000000..dd07906 --- /dev/null +++ b/002_source/cms/src/db/QuizLPQuestions/GetById.tsx @@ -0,0 +1,8 @@ +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizLPQuestionById(id: string): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).getOne(id); +} diff --git a/002_source/cms/src/db/QuizLPQuestions/GetHiddenCount.tsx b/002_source/cms/src/db/QuizLPQuestions/GetHiddenCount.tsx new file mode 100644 index 0000000..ed4768c --- /dev/null +++ b/002_source/cms/src/db/QuizLPQuestions/GetHiddenCount.tsx @@ -0,0 +1,14 @@ +// REQ0006 +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizLPQuestionsCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} diff --git a/002_source/cms/src/db/QuizLPQuestions/GetVisibleCount.tsx b/002_source/cms/src/db/QuizLPQuestions/GetVisibleCount.tsx new file mode 100644 index 0000000..26c5b78 --- /dev/null +++ b/002_source/cms/src/db/QuizLPQuestions/GetVisibleCount.tsx @@ -0,0 +1,14 @@ +// REQ0006 +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizLPQuestionsCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} diff --git a/002_source/cms/src/db/QuizLPQuestions/Update.tsx b/002_source/cms/src/db/QuizLPQuestions/Update.tsx new file mode 100644 index 0000000..d7982d4 --- /dev/null +++ b/002_source/cms/src/db/QuizLPQuestions/Update.tsx @@ -0,0 +1,9 @@ +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp_categories/type'; + +export default function updateQuizLPCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).update(id, data); +} diff --git a/002_source/cms/src/db/QuizLPQuestions/_PROMPT.md b/002_source/cms/src/db/QuizLPQuestions/_PROMPT.md new file mode 100644 index 0000000..01994fc --- /dev/null +++ b/002_source/cms/src/db/QuizLPQuestions/_PROMPT.md @@ -0,0 +1,18 @@ +please help to review the `tsx` file in this folder +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPQuestions` + +it was clone from +`LPCategories` +please help to modify to +`LPQuestions` + +please also help to modify the name of +`variables`, `constants`, `functions`, `classes`, components's name, paths + +the db fields structures between them are the same + +do not move the files +do not create directories +keep current folder structure is important + +thanks diff --git a/002_source/cms/src/db/schema.json b/002_source/cms/src/db/schema.json index 50aa4ef..ca9a657 100644 --- a/002_source/cms/src/db/schema.json +++ b/002_source/cms/src/db/schema.json @@ -1479,6 +1479,109 @@ "system": false, "type": "relation" }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2560465762", + "max": 0, + "min": 0, + "name": "slug", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "convertURLs": false, + "hidden": false, + "id": "editor1843675174", + "maxSize": 0, + "name": "description", + "presentable": false, + "required": false, + "system": false, + "type": "editor" + }, { "hidden": false, "id": "autodate2990389176", @@ -1685,6 +1788,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": [], @@ -2701,4 +2818,4 @@ "indexes": [], "system": false } -] +] \ No newline at end of file