From f65f6df6601cd958aae3ab45a84a55b973af3bfc Mon Sep 17 00:00:00 2001 From: louiscklaw Date: Mon, 21 Apr 2025 05:16:30 +0800 Subject: [PATCH] update for lp_categories, --- .../cms/scripts/{lint_w.sh => 001_lint_w.sh} | 0 002_source/cms/scripts/002_typecheck_w.sh | 6 + 002_source/cms/scripts/003_build_w.sh | 6 + 002_source/cms/scripts/build_w.sh | 5 - .../app/dashboard/Sample/BasicDetailCard.tsx | 44 +- .../app/dashboard/Sample/SampleTitleCard.tsx | 34 +- .../lesson_categories/edit/[cat_id]/page.tsx | 8 +- .../src/app/dashboard/lesson_types/page.tsx | 19 +- .../src/app/dashboard/lp_categories/PROMPT.md | 0 .../[cat_id]/BasicDetailCard.tsx | 79 +++ .../lp_categories/[cat_id]/TitleCard.tsx | 73 +++ .../{[lp_cat_id] => [cat_id]}/page.tsx | 96 ++-- .../dashboard/lp_categories/create/page.tsx | 13 +- .../lp_categories/edit/[cat_id]/_PROMPT.md | 11 + .../edit/{[typeId] => [cat_id]}/page.tsx | 13 +- .../lp-categories-sample-data.tsx | 90 ++++ .../dashboard/lp_categories/lp-categories.tsx | 155 ------ .../src/app/dashboard/lp_categories/page.tsx | 100 ++-- .../lesson-categories-table.tsx | 2 - .../dashboard/lesson_category/type.d.ts | 2 +- .../lesson_type/lesson-type-create-form.tsx | 7 +- .../lesson_type/lesson-types-pagination.tsx | 7 +- .../lesson_type/lesson-types-table.tsx | 82 +++- .../dashboard/lp_categories/_constants.ts | 65 +-- .../dashboard/lp_categories/helloworld.tsx | 3 - .../lp-categories-create-form.tsx | 388 --------------- .../lp_categories/lp-categories-filters.tsx | 35 +- .../lp-categories-pagination.tsx | 11 +- .../lp-categories-selection-context.tsx | 17 +- ...gory-table.tsx => lp-categories-table.tsx} | 105 +++- .../lp_categories/lp-category-create-form.tsx | 419 ++++++++++++++++ .../lp_categories/lp-category-edit-form.tsx | 454 +++++++++++++----- .../dashboard/lp_categories/notifications.tsx | 22 +- .../dashboard/lp_categories/payments.tsx | 12 +- .../lp_categories/shipping-address.tsx | 19 +- .../dashboard/lp_categories/type.d.ts | 31 +- .../summary/ActiveUserCount/index.tsx | 46 +- .../summary/LessonCategoriesCount/index.tsx | 38 +- .../summary/LessonTypeCount/index.tsx | 45 +- .../overview/summary/LoadingSummary/index.tsx | 81 ++++ 002_source/cms/src/constants.ts | 8 +- 002_source/cms/src/db/DB_AI_GUIDELINE.MD | 5 + .../cms/src/db/LessonCategories/Create.tsx | 4 +- .../src/db/LessonCategories/GetAllCount.tsx | 13 +- .../cms/src/db/LessonCategories/Update.tsx | 4 +- .../cms/src/db/QuizListenings/Delete.tsx | 4 +- .../cms/src/db/QuizListenings/GetAll.tsx | 4 +- .../cms/src/db/QuizListenings/GetAllCount.tsx | 4 +- .../cms/src/db/QuizListenings/GetById.tsx | 4 +- .../src/db/QuizListenings/GetHiddenCount.tsx | 6 +- .../src/db/QuizListenings/GetVisibleCount.tsx | 6 +- .../src/db/QuizListenings/ListWithOption.tsx | 4 +- .../cms/src/db/UserMetas/GetAllCount.tsx | 17 +- 002_source/cms/src/db/_PROMPT/1.MD | 23 + 002_source/cms/src/db/_PROMPT/2.MD | 35 ++ 002_source/cms/src/db/_PROMPT/3.MD | 47 ++ 002_source/cms/src/db/_PROMPT/4.md | 57 +++ 002_source/cms/src/db/_PROMPT/temp.md | 6 + 002_source/cms/src/db/schema.json | 55 ++- 002_source/cms/src/lib/file-to-base64.tsx | 17 + 60 files changed, 1919 insertions(+), 1047 deletions(-) rename 002_source/cms/scripts/{lint_w.sh => 001_lint_w.sh} (100%) create mode 100755 002_source/cms/scripts/002_typecheck_w.sh create mode 100755 002_source/cms/scripts/003_build_w.sh delete mode 100755 002_source/cms/scripts/build_w.sh delete mode 100644 002_source/cms/src/app/dashboard/lp_categories/PROMPT.md create mode 100644 002_source/cms/src/app/dashboard/lp_categories/[cat_id]/BasicDetailCard.tsx create mode 100644 002_source/cms/src/app/dashboard/lp_categories/[cat_id]/TitleCard.tsx rename 002_source/cms/src/app/dashboard/lp_categories/{[lp_cat_id] => [cat_id]}/page.tsx (54%) create mode 100644 002_source/cms/src/app/dashboard/lp_categories/edit/[cat_id]/_PROMPT.md rename 002_source/cms/src/app/dashboard/lp_categories/edit/{[typeId] => [cat_id]}/page.tsx (79%) create mode 100644 002_source/cms/src/app/dashboard/lp_categories/lp-categories-sample-data.tsx delete mode 100644 002_source/cms/src/app/dashboard/lp_categories/lp-categories.tsx delete mode 100644 002_source/cms/src/components/dashboard/lp_categories/helloworld.tsx delete mode 100644 002_source/cms/src/components/dashboard/lp_categories/lp-categories-create-form.tsx rename 002_source/cms/src/components/dashboard/lp_categories/{lp-category-table.tsx => lp-categories-table.tsx} (68%) create mode 100644 002_source/cms/src/components/dashboard/lp_categories/lp-category-create-form.tsx create mode 100644 002_source/cms/src/components/dashboard/overview/summary/LoadingSummary/index.tsx create mode 100644 002_source/cms/src/db/_PROMPT/1.MD create mode 100644 002_source/cms/src/db/_PROMPT/2.MD create mode 100644 002_source/cms/src/db/_PROMPT/3.MD create mode 100644 002_source/cms/src/db/_PROMPT/4.md create mode 100644 002_source/cms/src/db/_PROMPT/temp.md diff --git a/002_source/cms/scripts/lint_w.sh b/002_source/cms/scripts/001_lint_w.sh similarity index 100% rename from 002_source/cms/scripts/lint_w.sh rename to 002_source/cms/scripts/001_lint_w.sh diff --git a/002_source/cms/scripts/002_typecheck_w.sh b/002_source/cms/scripts/002_typecheck_w.sh new file mode 100755 index 0000000..95dad2a --- /dev/null +++ b/002_source/cms/scripts/002_typecheck_w.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -ex + +reset +pnpm run typecheck:w diff --git a/002_source/cms/scripts/003_build_w.sh b/002_source/cms/scripts/003_build_w.sh new file mode 100755 index 0000000..4836871 --- /dev/null +++ b/002_source/cms/scripts/003_build_w.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -ex + +reset +pnpm run build diff --git a/002_source/cms/scripts/build_w.sh b/002_source/cms/scripts/build_w.sh deleted file mode 100755 index 6baf1f6..0000000 --- a/002_source/cms/scripts/build_w.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -npx nodemon --ext tsx,ts --exec "reset && pnpm run build" diff --git a/002_source/cms/src/app/dashboard/Sample/BasicDetailCard.tsx b/002_source/cms/src/app/dashboard/Sample/BasicDetailCard.tsx index bc9e4d4..b238b17 100644 --- a/002_source/cms/src/app/dashboard/Sample/BasicDetailCard.tsx +++ b/002_source/cms/src/app/dashboard/Sample/BasicDetailCard.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import { useParams, useRouter } from 'next/navigation'; +import { useRouter } from 'next/navigation'; import Avatar from '@mui/material/Avatar'; import Card from '@mui/material/Card'; import CardHeader from '@mui/material/CardHeader'; @@ -15,7 +15,6 @@ import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; import { useTranslation } from 'react-i18next'; -import { paths } from '@/paths'; import { PropertyItem } from '@/components/core/property-item'; import { PropertyList } from '@/components/core/property-list'; @@ -27,7 +26,6 @@ export default function BasicDetailCard({ handleEditClick: () => void; }): React.JSX.Element { const { t } = useTranslation(); - const router = useRouter(); return ( @@ -48,10 +46,23 @@ export default function BasicDetailCard({ } title={t('list.basic-details')} /> - } orientation="vertical" sx={{ '--PropertyItem-padding': '12px 24px' }}> + } + orientation="vertical" + sx={{ '--PropertyItem-padding': '12px 24px' }} + > {( [ - { key: 'Customer ID', value: }, + { + key: 'Customer ID', + value: ( + + ), + }, { key: 'Name', value: 'Miron Vitold' }, { key: 'Email', value: 'miron.vitold@domain.com' }, { key: 'Phone', value: '(425) 434-5535' }, @@ -59,9 +70,20 @@ export default function BasicDetailCard({ { key: 'Quota', value: ( - - - + + + 50% @@ -70,7 +92,11 @@ export default function BasicDetailCard({ ] satisfies { key: string; value: React.ReactNode }[] ).map( (item): React.JSX.Element => ( - + ) )} diff --git a/002_source/cms/src/app/dashboard/Sample/SampleTitleCard.tsx b/002_source/cms/src/app/dashboard/Sample/SampleTitleCard.tsx index 62d0bfe..76273a9 100644 --- a/002_source/cms/src/app/dashboard/Sample/SampleTitleCard.tsx +++ b/002_source/cms/src/app/dashboard/Sample/SampleTitleCard.tsx @@ -15,27 +15,49 @@ export default function SampleTitleCard(): React.JSX.Element { return ( <> - - + + empty
- + {t('list.customer-name')} } + icon={ + + } label={t('list.active')} size="small" variant="outlined" /> - + {t('list.customer-email')}
-
diff --git a/002_source/cms/src/app/dashboard/lesson_categories/edit/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/lesson_categories/edit/[cat_id]/page.tsx index fb4b074..88737a6 100644 --- a/002_source/cms/src/app/dashboard/lesson_categories/edit/[cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/lesson_categories/edit/[cat_id]/page.tsx @@ -12,14 +12,8 @@ import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; import { LessonCategoryEditForm } from '@/components/dashboard/lesson_category/lesson-category-edit-form'; -// import { LessonCategoryEditForm } from '@/components/dashboard/lesson_category/lesson-category-edit-form'; - export default function Page(): React.JSX.Element { - const { t } = useTranslation(['common', 'lesson_category']); - - React.useEffect(() => { - console.log('helloworld'); - }, []); + const { t } = useTranslation(['lesson_category']); return ( (false); const [showLoading, setShowLoading] = React.useState(true); const [showError, setShowError] = React.useState(false); + // const [rowsPerPage, setRowsPerPage] = React.useState(5); const [f, setF] = React.useState([]); const [currentPage, setCurrentPage] = React.useState(0); @@ -102,7 +105,11 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { if (showError) return ( - + ); return ( @@ -115,7 +122,11 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { }} > - + {t('list.title')} diff --git a/002_source/cms/src/app/dashboard/lp_categories/PROMPT.md b/002_source/cms/src/app/dashboard/lp_categories/PROMPT.md deleted file mode 100644 index e69de29..0000000 diff --git a/002_source/cms/src/app/dashboard/lp_categories/[cat_id]/BasicDetailCard.tsx b/002_source/cms/src/app/dashboard/lp_categories/[cat_id]/BasicDetailCard.tsx new file mode 100644 index 0000000..176fc8c --- /dev/null +++ b/002_source/cms/src/app/dashboard/lp_categories/[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 { LpCategory } from '@/components/dashboard/lp_categories/type'; + +export default function BasicDetailCard({ + lpModel: model, + handleEditClick, +}: { + lpModel: LpCategory; + 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.cat_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_categories/[cat_id]/TitleCard.tsx b/002_source/cms/src/app/dashboard/lp_categories/[cat_id]/TitleCard.tsx new file mode 100644 index 0000000..e0312e6 --- /dev/null +++ b/002_source/cms/src/app/dashboard/lp_categories/[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 { LpCategory } from '@/components/dashboard/lp_categories/type'; + +function getImageUrlFrRecord(record) { + return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; +} + +export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element { + const { t } = useTranslation(); + + return ( + <> + + + {t('empty')} + +
+ + {lpModel.cat_name} + + } + label={lpModel.visible} + size="small" + variant="outlined" + /> + + + {lpModel.slug} + +
+
+
+ +
+ + ); +} diff --git a/002_source/cms/src/app/dashboard/lp_categories/[lp_cat_id]/page.tsx b/002_source/cms/src/app/dashboard/lp_categories/[cat_id]/page.tsx similarity index 54% rename from 002_source/cms/src/app/dashboard/lp_categories/[lp_cat_id]/page.tsx rename to 002_source/cms/src/app/dashboard/lp_categories/[cat_id]/page.tsx index 50cb497..2e9443c 100644 --- a/002_source/cms/src/app/dashboard/lp_categories/[lp_cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/lp_categories/[cat_id]/page.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import RouterLink from 'next/link'; import { useParams, useRouter } from 'next/navigation'; -import getQuizListeningById from '@/db/QuizListenings/GetById'; +import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import Box from '@mui/material/Box'; import Link from '@mui/material/Link'; import Stack from '@mui/material/Stack'; @@ -12,65 +12,67 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow import type { RecordModel } from 'pocketbase'; import { useTranslation } from 'react-i18next'; -// import type { LpCategory } from '@/types/type.d'; 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 { defaultLpCategory, LpCategoryDefaultValue } from '@/components/dashboard/lp_categories/_constants'; +import { defaultLpCategory } from '@/components/dashboard/lp_categories/_constants.ts'; import { Notifications } from '@/components/dashboard/lp_categories/notifications'; -import { LpCategory } from '@/components/dashboard/lp_categories/type'; +import type { LpCategory } from '@/components/dashboard/lp_categories/type'; import FormLoading from '@/components/loading'; import SampleAddressCard from '../../Sample/AddressCard'; -import BasicDetailCard from '../../Sample/BasicDetailCard'; import { SampleNotifications } from '../../Sample/Notifications'; import SamplePaymentCard from '../../Sample/SamplePaymentCard'; import SampleSecurityCard from '../../Sample/SampleSecurityCard'; -import SampleTitleCard from '../../Sample/SampleTitleCard'; +import BasicDetailCard from './BasicDetailCard'; +import TitleCard from './TitleCard'; export default function Page(): React.JSX.Element { - const { t } = useTranslation(['listening_practice']); + const { t } = useTranslation(); const router = useRouter(); // - const { lp_cat_id: lpCatId } = useParams<{ lp_cat_id: string }>(); - + const { cat_id: catId } = useParams<{ cat_id: string }>(); // const [showLoading, setShowLoading] = React.useState(true); - const [showError, setShowError] = React.useState(false); - const [errorDetails, setErrorDetails] = React.useState(''); + const [showError, setShowError] = React.useState({ show: false, detail: '' }); // - const [showLessonType, setShowLessonType] = React.useState(LpCategoryDefaultValue.default); + const [showLessonCategory, setShowLessonCategory] = React.useState(defaultLpCategory); - function handleEditClick(): void { - router.push(paths.dashboard.lp_categories.edit(lpCatId)); + function handleEditClick() { + router.push(paths.dashboard.lp_categories.edit(showLessonCategory.id)); } + const [lpModel, setLpModel] = React.useState(null); React.useEffect(() => { - getQuizListeningById(lpCatId) - .then((model: RecordModel) => { - setShowLessonType({ ...defaultLpCategory, ...model }); - }) - .catch((err) => { - logger.error(err); - toast(t('list.error')); - setErrorDetails(err); - setShowError(true); - }) - .finally(() => { - setShowLoading(false); - }); - }, [lpCatId]); + if (catId) { + pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES) + .getOne(catId) + .then((model: RecordModel) => { + setShowLessonCategory({ ...defaultLpCategory, ...model }); + setLpModel(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) + if (showError.show) return ( ); @@ -89,7 +91,7 @@ export default function Page(): React.JSX.Element { @@ -97,18 +99,34 @@ export default function Page(): React.JSX.Element { {t('list.title')} - - + + - - + + - + - + diff --git a/002_source/cms/src/app/dashboard/lp_categories/create/page.tsx b/002_source/cms/src/app/dashboard/lp_categories/create/page.tsx index f9c08ed..a3e7882 100644 --- a/002_source/cms/src/app/dashboard/lp_categories/create/page.tsx +++ b/002_source/cms/src/app/dashboard/lp_categories/create/page.tsx @@ -1,5 +1,8 @@ 'use client'; +// RULES: +// T.B.A. +// import * as React from 'react'; import RouterLink from 'next/link'; import Box from '@mui/material/Box'; @@ -10,10 +13,12 @@ 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-categories-create-form'; +import { LpCategoryCreateForm } from '@/components/dashboard/lp_categories/lp-category-create-form'; export default function Page(): React.JSX.Element { - const { t } = useTranslation(['lp_category']); + // RULES: follow the name of page directory + const { t } = useTranslation(['lp_categories']); + return ( - {t('list.title')} + {t('title')}
diff --git a/002_source/cms/src/app/dashboard/lp_categories/edit/[cat_id]/_PROMPT.md b/002_source/cms/src/app/dashboard/lp_categories/edit/[cat_id]/_PROMPT.md new file mode 100644 index 0000000..abf4465 --- /dev/null +++ b/002_source/cms/src/app/dashboard/lp_categories/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_categories/edit/[typeId]/page.tsx b/002_source/cms/src/app/dashboard/lp_categories/edit/[cat_id]/page.tsx similarity index 79% rename from 002_source/cms/src/app/dashboard/lp_categories/edit/[typeId]/page.tsx rename to 002_source/cms/src/app/dashboard/lp_categories/edit/[cat_id]/page.tsx index 2261152..9f7f258 100644 --- a/002_source/cms/src/app/dashboard/lp_categories/edit/[typeId]/page.tsx +++ b/002_source/cms/src/app/dashboard/lp_categories/edit/[cat_id]/page.tsx @@ -10,11 +10,14 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; -// import { LessonTypeEditForm } from '@/components/dashboard/lesson_type/lesson-type-edit-form'; import { LpCategoryEditForm } from '@/components/dashboard/lp_categories/lp-category-edit-form'; export default function Page(): React.JSX.Element { - const { t } = useTranslation(); + const { t } = useTranslation(['lp_categories']); + + React.useEffect(() => { + // console.log('helloworld'); + }, []); return ( - {t('dashboard.lessonTypes.title')} + {t('edit.title')}
- {t('dashboard.lessonTypes.edit.title')} + {t('edit.title')}
diff --git a/002_source/cms/src/app/dashboard/lp_categories/lp-categories-sample-data.tsx b/002_source/cms/src/app/dashboard/lp_categories/lp-categories-sample-data.tsx new file mode 100644 index 0000000..0c1aa79 --- /dev/null +++ b/002_source/cms/src/app/dashboard/lp_categories/lp-categories-sample-data.tsx @@ -0,0 +1,90 @@ +import { dayjs } from '@/lib/dayjs'; +import { LessonCategory } from '@/components/dashboard/lesson_category/type'; + +export const LpCategoriesSampleData = [ + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, +] satisfies LessonCategory[]; diff --git a/002_source/cms/src/app/dashboard/lp_categories/lp-categories.tsx b/002_source/cms/src/app/dashboard/lp_categories/lp-categories.tsx deleted file mode 100644 index 41f39fb..0000000 --- a/002_source/cms/src/app/dashboard/lp_categories/lp-categories.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { dayjs } from '@/lib/dayjs'; -import type { Customer } from '@/components/dashboard/customer/customers-table'; - -export const LpCategories = [ - { - id: 'USR-005', - name: 'Fran Perez', - avatar: '/assets/avatar-5.png', - email: 'fran.perez@domain.com', - phone: '(815) 704-0045', - quota: 50, - status: 'active', - createdAt: dayjs().subtract(1, 'hour').toDate(), - }, - { - id: 'USR-004', - name: 'Penjani Inyene', - avatar: '/assets/avatar-4.png', - email: 'penjani.inyene@domain.com', - phone: '(803) 937-8925', - quota: 100, - status: 'active', - createdAt: dayjs().subtract(3, 'hour').toDate(), - }, - { - id: 'USR-003', - name: 'Carson Darrin', - avatar: '/assets/avatar-3.png', - email: 'carson.darrin@domain.com', - phone: '(715) 278-5041', - quota: 10, - status: 'blocked', - createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), - }, - { - id: 'USR-002', - name: 'Siegbert Gottfried', - avatar: '/assets/avatar-2.png', - email: 'siegbert.gottfried@domain.com', - phone: '(603) 766-0431', - quota: 0, - status: 'pending', - createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), - }, - { - id: 'USR-001', - name: 'Miron Vitold', - avatar: '/assets/avatar-1.png', - email: 'miron.vitold@domain.com', - phone: '(425) 434-5535', - quota: 50, - status: 'active', - createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), - }, - { - id: 'USR-005', - name: 'Fran Perez', - avatar: '/assets/avatar-5.png', - email: 'fran.perez@domain.com', - phone: '(815) 704-0045', - quota: 50, - status: 'active', - createdAt: dayjs().subtract(1, 'hour').toDate(), - }, - { - id: 'USR-004', - name: 'Penjani Inyene', - avatar: '/assets/avatar-4.png', - email: 'penjani.inyene@domain.com', - phone: '(803) 937-8925', - quota: 100, - status: 'active', - createdAt: dayjs().subtract(3, 'hour').toDate(), - }, - { - id: 'USR-003', - name: 'Carson Darrin', - avatar: '/assets/avatar-3.png', - email: 'carson.darrin@domain.com', - phone: '(715) 278-5041', - quota: 10, - status: 'blocked', - createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), - }, - { - id: 'USR-002', - name: 'Siegbert Gottfried', - avatar: '/assets/avatar-2.png', - email: 'siegbert.gottfried@domain.com', - phone: '(603) 766-0431', - quota: 0, - status: 'pending', - createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), - }, - { - id: 'USR-001', - name: 'Miron Vitold', - avatar: '/assets/avatar-1.png', - email: 'miron.vitold@domain.com', - phone: '(425) 434-5535', - quota: 50, - status: 'active', - createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), - }, - { - id: 'USR-005', - name: 'Fran Perez', - avatar: '/assets/avatar-5.png', - email: 'fran.perez@domain.com', - phone: '(815) 704-0045', - quota: 50, - status: 'active', - createdAt: dayjs().subtract(1, 'hour').toDate(), - }, - { - id: 'USR-004', - name: 'Penjani Inyene', - avatar: '/assets/avatar-4.png', - email: 'penjani.inyene@domain.com', - phone: '(803) 937-8925', - quota: 100, - status: 'active', - createdAt: dayjs().subtract(3, 'hour').toDate(), - }, - { - id: 'USR-003', - name: 'Carson Darrin', - avatar: '/assets/avatar-3.png', - email: 'carson.darrin@domain.com', - phone: '(715) 278-5041', - quota: 10, - status: 'blocked', - createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), - }, - { - id: 'USR-002', - name: 'Siegbert Gottfried', - avatar: '/assets/avatar-2.png', - email: 'siegbert.gottfried@domain.com', - phone: '(603) 766-0431', - quota: 0, - status: 'pending', - createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), - }, - { - id: 'USR-001', - name: 'Miron Vitold', - avatar: '/assets/avatar-1.png', - email: 'miron.vitold@domain.com', - phone: '(425) 434-5535', - quota: 50, - status: 'active', - createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), - }, -] satisfies Customer[]; diff --git a/002_source/cms/src/app/dashboard/lp_categories/page.tsx b/002_source/cms/src/app/dashboard/lp_categories/page.tsx index 7293c6d..ad1d24f 100644 --- a/002_source/cms/src/app/dashboard/lp_categories/page.tsx +++ b/002_source/cms/src/app/dashboard/lp_categories/page.tsx @@ -1,8 +1,12 @@ 'use client'; +// RULES: +// contains list page for lp_categories (QuizLPCategories) +// contain definition to collection only +// import * as React from 'react'; import { useRouter } from 'next/navigation'; -import listWithOption from '@/db/QuizListenings/ListWithOption'; +import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { LoadingButton } from '@mui/lab'; import Box from '@mui/material/Box'; import Card from '@mui/material/Card'; @@ -13,54 +17,59 @@ import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; import type { ListResult, RecordModel } from 'pocketbase'; import { useTranslation } from 'react-i18next'; -// import type { LpCategory } from '@/types/type.d'; 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 { 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-category-table'; -import { LpCategory } from '@/components/dashboard/lp_categories/type'; +import { LpCategoriesTable } from '@/components/dashboard/lp_categories/lp-categories-table'; +import type { LpCategory } from '@/components/dashboard/lp_categories/type'; import FormLoading from '@/components/loading'; export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { t } = useTranslation(['listening_practice']); + const { t } = useTranslation(['lp_categories']); const { email, phone, sortDir, status, name, visible, type } = searchParams; const router = useRouter(); - const [lpCategoriesData, setLpCategoriesData] = React.useState([]); + const [lessonCategoriesData, setLessonCategoriesData] = React.useState([]); // 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([]); - const [currentPage, setCurrentPage] = React.useState(0); + const [currentPage, setCurrentPage] = React.useState(1); const [recordCount, setRecordCount] = React.useState(0); const [listOption, setListOption] = React.useState({}); const [listSort, setListSort] = React.useState({}); // + const sortedLessonCategories = applySort(lessonCategoriesData, sortDir); + const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status }); + const reloadRows = async (): Promise => { try { - const models: ListResult = await listWithOption({ - currentPage, - rowsPerPage, - listOption, - }); - + const models: ListResult = await pb + .collection(COL_LISTENINGS_PRACTICE_CATEGORIES) + .getList(currentPage + 1, rowsPerPage, {}); const { items, totalItems } = models; - const tempLpCategories: LpCategory[] = items.map((lt) => { + const tempLessonTypes: LpCategory[] = items.map((lt) => { return { ...defaultLpCategory, ...lt }; }); - setLpCategoriesData(tempLpCategories); + setLessonCategoriesData(tempLessonTypes); setRecordCount(totalItems); - setF(tempLpCategories); + setF(tempLessonTypes); + console.log({ currentPage, f }); } catch (error) { // + setShowError({ show: true, detail: JSON.stringify(error) }); } finally { setShowLoading(false); } @@ -70,38 +79,15 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { void reloadRows(); }, [currentPage, rowsPerPage, listOption]); - React.useEffect(() => { - let tempFilter = [], - tempSortDir = ''; + if (showLoading) return ; - if (visible) { - tempFilter.push(`visible = "${visible}"`); - } - - if (sortDir) { - tempSortDir = `-created`; - } - - if (name) { - tempFilter.push(`name ~ "%${name}%"`); - } - - if (type) { - tempFilter.push(`type ~ "%${type}%"`); - } - - setListOption({ - filter: tempFilter.join(' && '), - sort: tempSortDir, - // - }); - }, [visible, sortDir, name, type]); - - if (f.length === 0 || showLoading) return ; - - if (showError) + if (showError.show) return ( - + ); return ( @@ -113,8 +99,14 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { width: 'var(--Content-width)', }} > + {JSON.stringify({ currentPage, rowsPerPage })} + - + {t('list.title')} @@ -128,24 +120,22 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { startIcon={} variant="contained" > - {t('add')} + {t('list.add')}
- + diff --git a/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-table.tsx b/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-table.tsx index 590975b..b217506 100644 --- a/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-table.tsx +++ b/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-table.tsx @@ -25,9 +25,7 @@ import { dayjs } from '@/lib/dayjs'; import { DataTable } from '@/components/core/data-table'; import type { ColumnDef } from '@/components/core/data-table'; -// import { LessonCategory } from '../lp_categories/type'; import ConfirmDeleteModal from './confirm-delete-modal'; -// import type { LessonCategory } from './interfaces.1ts'; import { useLessonCategoriesSelection } from './lesson-categories-selection-context'; import { LessonCategory } from './type'; diff --git a/002_source/cms/src/components/dashboard/lesson_category/type.d.ts b/002_source/cms/src/components/dashboard/lesson_category/type.d.ts index 7fe378b..4ea1ce5 100644 --- a/002_source/cms/src/components/dashboard/lesson_category/type.d.ts +++ b/002_source/cms/src/components/dashboard/lesson_category/type.d.ts @@ -12,6 +12,7 @@ export interface LessonCategory { lesson_id: string; description: string; remarks: string; + createdAt: Date; // name: string; avatar: string; @@ -19,7 +20,6 @@ export interface LessonCategory { phone: string; quota: number; status: 'pending' | 'active' | 'blocked' | 'NA'; - createdAt: Date; } export interface CreateForm { diff --git a/002_source/cms/src/components/dashboard/lesson_type/lesson-type-create-form.tsx b/002_source/cms/src/components/dashboard/lesson_type/lesson-type-create-form.tsx index f4b64f5..0baa7ef 100644 --- a/002_source/cms/src/components/dashboard/lesson_type/lesson-type-create-form.tsx +++ b/002_source/cms/src/components/dashboard/lesson_type/lesson-type-create-form.tsx @@ -39,11 +39,6 @@ import { toast } from '@/components/core/toaster'; import { LessonTypeCreateFormDefault } from './_constants'; import { CreateForm } from './lesson-type'; -// import { CreateForm, LessonTypeCreateFormDefault } from './interfaces'; - -// import { createLessonType } from './http-actions'; -// import { LessonTypeCreateForm, LessonTypeCreateFormDefault } from './interfaces'; - const schema = zod.object({ name: zod.string().min(1, 'Name is required').max(255), type: zod.string().min(1, 'Name is required').max(255), @@ -94,6 +89,8 @@ export function LessonTypeCreateForm(): React.JSX.Element { setIsCreating(false); }); }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps [router] ); diff --git a/002_source/cms/src/components/dashboard/lesson_type/lesson-types-pagination.tsx b/002_source/cms/src/components/dashboard/lesson_type/lesson-types-pagination.tsx index fabe511..f687d95 100644 --- a/002_source/cms/src/components/dashboard/lesson_type/lesson-types-pagination.tsx +++ b/002_source/cms/src/components/dashboard/lesson_type/lesson-types-pagination.tsx @@ -10,6 +10,7 @@ function noop(): void { interface LessonTypesPaginationProps { count: number; page: number; + // setPage: (page: number) => void; setRowsPerPage: (page: number) => void; rowsPerPage: number; @@ -18,6 +19,7 @@ interface LessonTypesPaginationProps { export function LessonTypesPagination({ count, page, + // setPage, setRowsPerPage, rowsPerPage, @@ -37,12 +39,11 @@ export function LessonTypesPagination({ ); } diff --git a/002_source/cms/src/components/dashboard/lesson_type/lesson-types-table.tsx b/002_source/cms/src/components/dashboard/lesson_type/lesson-types-table.tsx index dcc6e99..5988ae5 100644 --- a/002_source/cms/src/components/dashboard/lesson_type/lesson-types-table.tsx +++ b/002_source/cms/src/components/dashboard/lesson_type/lesson-types-table.tsx @@ -5,7 +5,6 @@ import RouterLink from 'next/link'; import Avatar from '@mui/material/Avatar'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; -import Chip from '@mui/material/Chip'; import IconButton from '@mui/material/IconButton'; import LinearProgress from '@mui/material/LinearProgress'; import Link from '@mui/material/Link'; @@ -26,15 +25,18 @@ import { DataTable } from '@/components/core/data-table'; import type { ColumnDef } from '@/components/core/data-table'; import ConfirmDeleteModal from './confirm-delete-modal'; -import { LessonType } from './lesson-type'; -// import type { LessonType } from './ILessonType'; +import type { LessonType } from './lesson-type'; import { useLessonTypesSelection } from './lesson-types-selection-context'; function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { return [ { formatter: (row): React.JSX.Element => ( - + void): ColumnDef ( - +
void): ColumnDef ( - +
void): ColumnDef { - // eslint-disable-next-line react-hooks/rules-of-hooks // const { t } = useTranslation(); const mapping = { - active: { label: 'Active', icon: }, + active: { + label: 'Active', + icon: ( + + ), + }, blocked: { label: 'Blocked', icon: }, - pending: { label: 'Pending', icon: }, + pending: { + label: 'Pending', + icon: ( + + ), + }, visible: { label: 'visible', - icon: , + icon: ( + + ), }, hidden: { label: 'hidden', - icon: , + icon: ( + + ), }, no_value: { label: 'no_value', - icon: , + icon: ( + + ), }, } as const; @@ -136,7 +176,10 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef ( - + - + columns={columns(handleDeleteClick)} onDeselectAll={deselectAll} @@ -200,7 +248,11 @@ export function LessonTypesTable({ rows, reloadRows }: LessonTypesTableProps): R /> {!rows.length ? ( - + {/* TODO: use hyphen here */} {t('no-lesson-types-found')} diff --git a/002_source/cms/src/components/dashboard/lp_categories/_constants.ts b/002_source/cms/src/components/dashboard/lp_categories/_constants.ts index 028d89b..991f2f1 100644 --- a/002_source/cms/src/components/dashboard/lp_categories/_constants.ts +++ b/002_source/cms/src/components/dashboard/lp_categories/_constants.ts @@ -1,57 +1,42 @@ -import { NO_NUM, NO_VALUE } from '@/constants'; -import type { RecordModel } from 'pocketbase'; - import { dayjs } from '@/lib/dayjs'; -import type { CreateForm, LpCategory } from './type'; - -// import type { CreateForm, LpCategory } from './types'; - -export const LpCategoryCreateFormDefault: CreateForm = { - name: '', - type: '', - pos: 1, - visible: 'visible', - description: '', - isActive: true, - order: 1, - imageUrl: '', -}; +import { CreateFormProps, LpCategory } from './type'; export const defaultLpCategory: LpCategory = { - id: '', - collectionId: '', + 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', // - cat_name: '', - cat_image_url: '', - cat_image: '', - pos: 1, - lesson_id: '1', - description: '', - remarks: '', - createdAt: dayjs().toDate(), - visible: 'visible', + collectionId: '0000000000', + createdAt: dayjs('2099-01-01').toDate(), // name: '', avatar: '', email: '', phone: '', quota: 0, - order: 1, status: 'NA', - isActive: true, - imageUrl: '', }; +// export const LpCategoryCreateFormDefault: CreateFormProps = { +// name: '', +// type: '', +// pos: 1, +// visible: 'visible', +// description: '', +// isActive: true, +// order: 1, +// imageUrl: '', +// }; + export const emptyLpCategory: LpCategory = { ...defaultLpCategory, - isActive: false, + isEmpty: true, }; - -export const LpCategoryDefaultValue = { - createForm: LpCategoryCreateFormDefault, - default: defaultLpCategory, - empty: emptyLpCategory, -}; - -export default LpCategoryDefaultValue; diff --git a/002_source/cms/src/components/dashboard/lp_categories/helloworld.tsx b/002_source/cms/src/components/dashboard/lp_categories/helloworld.tsx deleted file mode 100644 index 3989cb1..0000000 --- a/002_source/cms/src/components/dashboard/lp_categories/helloworld.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const helloworld = 'helloworld'; - -export { helloworld }; diff --git a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-create-form.tsx b/002_source/cms/src/components/dashboard/lp_categories/lp-categories-create-form.tsx deleted file mode 100644 index 6555889..0000000 --- a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-create-form.tsx +++ /dev/null @@ -1,388 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useRouter } from 'next/navigation'; -import { zodResolver } from '@hookform/resolvers/zod'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardActions from '@mui/material/CardActions'; -import CardContent from '@mui/material/CardContent'; -import Checkbox from '@mui/material/Checkbox'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import FormHelperText from '@mui/material/FormHelperText'; -import InputLabel from '@mui/material/InputLabel'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Select from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import Grid from '@mui/material/Unstable_Grid2'; -import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; -import { Controller, useForm } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; -import { z as zod } from 'zod'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { fileToBase64 } from '@/lib/file-to-base64'; -import { Option } from '@/components/core/option'; -import { toast } from '@/components/core/toaster'; - -const schema = zod.object({ - avatar: zod.string().optional(), - name: zod.string().min(1, 'Name is required').max(255), - email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255), - phone: zod.string().min(1, 'Phone is required').max(15), - company: zod.string().max(255), - billingAddress: zod.object({ - country: zod.string().min(1, 'Country is required').max(255), - state: zod.string().min(1, 'State is required').max(255), - city: zod.string().min(1, 'City is required').max(255), - zipCode: zod.string().min(1, 'Zip code is required').max(255), - line1: zod.string().min(1, 'Street line 1 is required').max(255), - line2: zod.string().max(255).optional(), - }), - taxId: zod.string().max(255).optional(), - timezone: zod.string().min(1, 'Timezone is required').max(255), - language: zod.string().min(1, 'Language is required').max(255), - currency: zod.string().min(1, 'Currency is required').max(255), -}); - -const defaultValues = { - avatar: '', - name: '', - email: '', - phone: '', - company: '', - billingAddress: { country: '', state: '', city: '', zipCode: '', line1: '', line2: '' }, - taxId: '', - timezone: 'new_york', - language: 'en', - currency: 'USD', -} satisfies Values; - -export type Values = zod.infer; - -export function LpCategoryCreateForm(): React.JSX.Element { - const router = useRouter(); - const { t } = useTranslation(); - - const { - control, - handleSubmit, - formState: { errors }, - setValue, - watch, - } = useForm({ defaultValues, resolver: zodResolver(schema) }); - - const onSubmit = React.useCallback( - async (_: Values): Promise => { - try { - // Make API request - toast.success('Customer updated'); - router.push(paths.dashboard.customers.details('1')); - } catch (err) { - logger.error(err); - toast.error('Something went wrong!'); - } - }, - [router] - ); - - const avatarInputRef = React.useRef(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('Account information')} - - - - - - - - - - {t('Avatar')} - {t('Min 400x400px, PNG or JPEG')} - - - - - - - ( - - {t('Name')} - - {errors.name ? {errors.name.message} : null} - - )} - /> - - - ( - - {t('Email address')} - - {errors.email ? {errors.email.message} : null} - - )} - /> - - - ( - - {t('Phone number')} - - {errors.phone ? {errors.phone.message} : null} - - )} - /> - - - ( - - {t('Company')} - - {errors.company ? {errors.company.message} : null} - - )} - /> - - - - - {t('Billing information')} - - - ( - - {t('Country')} - - {errors.billingAddress?.country ? ( - {errors.billingAddress?.country?.message} - ) : null} - - )} - /> - - - ( - - {t('State')} - - {errors.billingAddress?.state ? ( - {errors.billingAddress?.state?.message} - ) : null} - - )} - /> - - - ( - - {t('City')} - - {errors.billingAddress?.city ? ( - {errors.billingAddress?.city?.message} - ) : null} - - )} - /> - - - ( - - {t('Zip code')} - - {errors.billingAddress?.zipCode ? ( - {errors.billingAddress?.zipCode?.message} - ) : null} - - )} - /> - - - ( - - {t('Address')} - - {errors.billingAddress?.line1 ? ( - {errors.billingAddress?.line1?.message} - ) : null} - - )} - /> - - - ( - - {t('Tax ID')} - - {errors.taxId ? {errors.taxId.message} : null} - - )} - /> - - - - - {t('Shipping information')} - } label={t('Same as billing address')} /> - - - {t('Additional information')} - - - ( - - {t('Timezone')} - - {errors.timezone ? {errors.timezone.message} : null} - - )} - /> - - - ( - - {t('Language')} - - {errors.language ? {errors.language.message} : null} - - )} - /> - - - ( - - {t('Currency')} - - {errors.currency ? {errors.currency.message} : null} - - )} - /> - - - - - - - - - - -
- ); -} diff --git a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-filters.tsx b/002_source/cms/src/components/dashboard/lp_categories/lp-categories-filters.tsx index ecd927c..efa3c3a 100644 --- a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-filters.tsx +++ b/002_source/cms/src/components/dashboard/lp_categories/lp-categories-filters.tsx @@ -2,6 +2,7 @@ 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'; @@ -19,12 +20,13 @@ import Typography from '@mui/material/Typography'; import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; +// import { pb } from '@/lib/pb'; import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; import { Option } from '@/components/core/option'; +// import { LessonCategory } from '../lp_categories/type'; import { useLpCategoriesSelection } from './lp-categories-selection-context'; -// import type { LpCategory } from '@/types/type.d'; -import type { LpCategory } from './type'; +import { LpCategory } from './type'; export interface Filters { email?: string; @@ -59,6 +61,18 @@ export function LpCategoriesFilters({ const selection = useLpCategoriesSelection(); + function getVisible(): number { + return fullData.reduce((count, item: LpCategory) => { + return item.visible === 'visible' ? count + 1 : count; + }, 0); + } + + function getHidden(): number { + return fullData.reduce((count, item: LpCategory) => { + return item.visible === 'hidden' ? count + 1 : count; + }, 0); + } + // The tabs should be generated using API data. const tabs = [ { label: t('All'), value: '', count: totalCount }, @@ -101,6 +115,7 @@ export function LpCategoriesFilters({ searchParams.set('visible', newFilters.visible); } + // NOTE: modify according to COLLECTION router.push(`${paths.dashboard.lp_categories.list}?${searchParams.toString()}`); }, [router] @@ -181,13 +196,7 @@ export function LpCategoriesFilters({ return (
- + {tabs.map((tab) => ( } @@ -316,7 +325,7 @@ function NameFilterPopover(): React.JSX.Element { }} variant="contained" > - {t('Apply')} + {t('apply')} ); @@ -325,6 +334,7 @@ function NameFilterPopover(): React.JSX.Element { 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) ?? ''); @@ -351,7 +361,7 @@ function EmailFilterPopover(): React.JSX.Element { }} variant="contained" > - Apply + {t('Apply')} ); @@ -360,6 +370,7 @@ function EmailFilterPopover(): React.JSX.Element { 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) ?? ''); @@ -386,7 +397,7 @@ function PhoneFilterPopover(): React.JSX.Element { }} variant="contained" > - Apply + {t('Apply')} ); diff --git a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-pagination.tsx b/002_source/cms/src/components/dashboard/lp_categories/lp-categories-pagination.tsx index c8268bc..bf40481 100644 --- a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-pagination.tsx +++ b/002_source/cms/src/components/dashboard/lp_categories/lp-categories-pagination.tsx @@ -7,9 +7,10 @@ function noop(): void { return undefined; } -interface LpCategoriesPaginationProps { +interface LessonCategoriesPaginationProps { count: number; page: number; + // setPage: (page: number) => void; setRowsPerPage: (page: number) => void; rowsPerPage: number; @@ -18,10 +19,11 @@ interface LpCategoriesPaginationProps { export function LpCategoriesPagination({ count, page, + // setPage, setRowsPerPage, rowsPerPage, -}: LpCategoriesPaginationProps): React.JSX.Element { +}: 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) => { @@ -37,12 +39,11 @@ export function LpCategoriesPagination({ ); } diff --git a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-selection-context.tsx b/002_source/cms/src/components/dashboard/lp_categories/lp-categories-selection-context.tsx index cac36cc..a6c6502 100644 --- a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-selection-context.tsx +++ b/002_source/cms/src/components/dashboard/lp_categories/lp-categories-selection-context.tsx @@ -2,10 +2,11 @@ import * as React from 'react'; +// import type { LessonCategory } from '@/types/lesson-type'; import { useSelection } from '@/hooks/use-selection'; import type { Selection } from '@/hooks/use-selection'; -import type { LpCategory } from './type'; +import { LpCategory } from './type'; function noop(): void { return undefined; @@ -13,7 +14,7 @@ function noop(): void { export interface LpCategoriesSelectionContextValue extends Selection {} -export const CustomersSelectionContext = React.createContext({ +export const LpCategoriesSelectionContext = React.createContext({ deselectAll: noop, deselectOne: noop, selectAll: noop, @@ -25,19 +26,21 @@ export const CustomersSelectionContext = React.createContext customers.map((customer) => customer.id), [customers]); + const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]); const selection = useSelection(customerIds); - return {children}; + return ( + {children} + ); } export function useLpCategoriesSelection(): LpCategoriesSelectionContextValue { - return React.useContext(CustomersSelectionContext); + return React.useContext(LpCategoriesSelectionContext); } diff --git a/002_source/cms/src/components/dashboard/lp_categories/lp-category-table.tsx b/002_source/cms/src/components/dashboard/lp_categories/lp-categories-table.tsx similarity index 68% rename from 002_source/cms/src/components/dashboard/lp_categories/lp-category-table.tsx rename to 002_source/cms/src/components/dashboard/lp_categories/lp-categories-table.tsx index 5934d72..9209a82 100644 --- a/002_source/cms/src/components/dashboard/lp_categories/lp-category-table.tsx +++ b/002_source/cms/src/components/dashboard/lp_categories/lp-categories-table.tsx @@ -5,7 +5,6 @@ import RouterLink from 'next/link'; import Avatar from '@mui/material/Avatar'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; -import Chip from '@mui/material/Chip'; import IconButton from '@mui/material/IconButton'; import LinearProgress from '@mui/material/LinearProgress'; import Link from '@mui/material/Link'; @@ -33,7 +32,11 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef ( - + void): ColumnDef - - + + {' '}
{row.cat_name} - + slug: {row.cat_name}
@@ -60,12 +73,21 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef ( - - - - {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format( - row?.quota ? row.quota / 100 : 0 - )} + + + + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} ), @@ -76,13 +98,36 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef { // eslint-disable-next-line react-hooks/rules-of-hooks - const { t } = useTranslation(); const mapping = { - active: { label: 'Active', icon: }, + active: { + label: 'Active', + icon: ( + + ), + }, blocked: { label: 'Blocked', icon: }, - pending: { label: 'Pending', icon: }, - NA: { label: 'NA', icon: }, + pending: { + label: 'Pending', + icon: ( + + ), + }, + NA: { + label: 'NA', + icon: ( + + ), + }, } as const; const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; @@ -110,7 +155,10 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef ( - + void): ColumnDef void; } -function getCatImageFromId(row: LpCategory): string | undefined { - return `http://127.0.0.1:8090/api/files/${row.collectionId}/${row.id}/${row.cat_image}`; -} - -export function LpCategoriesTable({ rows, reloadRows }: LpCategoryTableProps): React.JSX.Element { - const { t } = useTranslation(); +export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element { + const { t } = useTranslation(['lp_categories']); const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpCategoriesSelection(); const [idToDelete, setIdToDelete] = React.useState(''); @@ -160,7 +204,12 @@ export function LpCategoriesTable({ rows, reloadRows }: LpCategoryTableProps): R return ( - + columns={columns(handleDeleteClick)} onDeselectAll={deselectAll} @@ -177,8 +226,12 @@ export function LpCategoriesTable({ rows, reloadRows }: LpCategoryTableProps): R /> {!rows.length ? ( - - {t('No customers found')} + + {t('no-lesson-categories-found')} ) : null} diff --git a/002_source/cms/src/components/dashboard/lp_categories/lp-category-create-form.tsx b/002_source/cms/src/components/dashboard/lp_categories/lp-category-create-form.tsx new file mode 100644 index 0000000..97c2beb --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp_categories/lp-category-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_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +import { Avatar, Divider, MenuItem, Select } from '@mui/material'; +// import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// import axios from 'axios'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import isDevelopment from '@/lib/check-is-development'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; + +import type { CreateFormProps } from './type'; + +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + cat_image: zod.array(zod.any()).optional(), + pos: zod.number().min(1, 'position is required').max(99), + init_answer: zod.string().optional(), + visible: zod.string(), + slug: zod.string().min(1, 'slug-is-required').max(255), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), + // TODO: remove me + type: zod.string().optional(), + isActive: zod.boolean().optional(), + order: zod.number().optional(), +}); + +type Values = zod.infer; + +export const defaultValues = { + cat_name: '', + cat_image: undefined, + pos: 1, + init_answer: '', + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function LpCategoryCreateForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['lp_categories']); + + const [isCreating, setIsCreating] = React.useState(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 = { + cat_name: values.cat_name, + cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null, + pos: values.pos, + init_answer: values.init_answer, + visible: values.visible, + slug: values.slug, + remarks: values.remarks, + description: values.description, + // + // TODO: remove me + type: 'type tet', + isActive: true, + order: 1, + }; + + try { + const result = await pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).create(payload); + + logger.debug(result); + toast.success(t('create.success')); + router.push(paths.dashboard.lp_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('create.failed')); + } finally { + setIsCreating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(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.cat_name')} + + {errors.cat_name ? {errors.cat_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_categories/lp-category-edit-form.tsx b/002_source/cms/src/components/dashboard/lp_categories/lp-category-edit-form.tsx index cfba900..024444d 100644 --- a/002_source/cms/src/components/dashboard/lp_categories/lp-category-edit-form.tsx +++ b/002_source/cms/src/components/dashboard/lp_categories/lp-category-edit-form.tsx @@ -3,88 +3,101 @@ import * as React from 'react'; import RouterLink from 'next/link'; import { useParams, useRouter } from 'next/navigation'; -import { COL_LESSON_TYPES } from '@/constants'; -import getQuizListeningById from '@/db/QuizListenings/GetById'; +// +import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { zodResolver } from '@hookform/resolvers/zod'; import { LoadingButton } from '@mui/lab'; -import { Avatar, MenuItem } from '@mui/material'; -// import Avatar from '@mui/material/Avatar'; +// +import Avatar from '@mui/material/Avatar'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Card from '@mui/material/Card'; import CardActions from '@mui/material/CardActions'; import CardContent from '@mui/material/CardContent'; -// import Checkbox from '@mui/material/Checkbox'; import Divider from '@mui/material/Divider'; import FormControl from '@mui/material/FormControl'; -// import FormControlLabel from '@mui/material/FormControlLabel'; import FormHelperText from '@mui/material/FormHelperText'; import InputLabel from '@mui/material/InputLabel'; +import 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 type { RecordModel } from 'pocketbase'; -import PocketBase from 'pocketbase'; +// +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 { Option } from '@/components/core/option'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; import { toast } from '@/components/core/toaster'; -import { EditFormProps } from '@/components/dashboard/lp_categories/type'; import FormLoading from '@/components/loading'; -import { LessonTypeEditFormProps } from '../lesson_type/lesson-type'; -import { defaultLpCategory } from './_constants'; - -// import { getLessonTypeById, updateLessonType } from './http-actions'; -// import { LessonTypeEditFormProps } from './types'; - -// import { defaultLessonType, type LessonTypeEditFormProps } from './interfaces'; - -// function fileToBase64(file: Blob): Promise { -// return new Promise((resolve, reject) => { -// const reader = new FileReader(); -// reader.readAsDataURL(file); -// reader.onload = () => { -// resolve(reader.result as string); -// }; -// reader.onerror = () => { -// reject(new Error('Error converting file to base64')); -// }; -// }); -// } +import ErrorDisplay from '../error'; +import type { EditFormProps } from './type'; +// TODO: review this const schema = zod.object({ - cat_name: zod.string().min(1, 'Name is required').max(255), - type: zod.string().min(1, 'Name is required').max(255), - slug: zod.string().min(1, 'Name is required').max(255), - pos: zod.number().min(1, 'Phone is required').max(15), - visible_to_user: zod.string().max(255), + cat_name: zod.string().min(1, 'name-is-required').max(255), + // accept file object when user change image + // accept http string when user not changing image + cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(), + + // position + pos: zod.number().min(1, 'position is required').max(99), + + // it should be a valid JSON + init_answer: zod + .string() + .refine( + (value) => { + try { + JSON.parse(value); + return true; + } catch (error) { + return false; + } + }, + { message: 'init_answer must be a valid JSON' } + ) + .optional(), + visible: zod.string(), + slug: zod.string().min(0, 'slug-is-required').max(255).optional(), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), }); type Values = zod.infer; const defaultValues = { cat_name: '', - slug: '', - type: '', + cat_image: undefined, pos: 1, - visible_to_user: 'visible', + init_answer: JSON.stringify({}), + visible: 'hidden', + slug: '', + remarks: '', + description: '', } satisfies Values; export function LpCategoryEditForm(): React.JSX.Element { const router = useRouter(); - const { t } = useTranslation(); - const { typeId } = useParams<{ typeId: string }>(); + const { t } = useTranslation(['lp_categories']); + + 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, @@ -95,92 +108,144 @@ export function LpCategoryEditForm(): React.JSX.Element { watch, } = useForm({ defaultValues, resolver: zodResolver(schema) }); - const onSubmit = React.useCallback(async (values: Values): Promise => { - setIsUpdating(true); - const tempUpdate: EditFormProps = { - cat_name: values.cat_name, - type: values.type, - pos: values.pos, - visible: values.visible_to_user ? 'visible' : 'hidden', - }; + const onSubmit = React.useCallback( + async (values: Values): Promise => { + setIsUpdating(true); - pb.collection(COL_LESSON_TYPES) - .update(typeId, tempUpdate) - .then((res) => { - logger.debug(res); - toast.success(t('dashboard.lessonTypes.update.success')); + const tempUpdate: EditFormProps = { + cat_name: values.cat_name, + cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null, + pos: values.pos, + 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_LISTENINGS_PRACTICE_CATEGORIES).update(catId, tempUpdate); + logger.debug(result); + toast.success(t('edit.success')); + router.push(paths.dashboard.lp_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('update.failed')); + } finally { setIsUpdating(false); - router.push(paths.dashboard.lesson_types.list); - }) - .catch((err) => { - logger.error(err); - toast.error('Something went wrong!'); - 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 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); + const url = await fileToBase64(file); + setValue('avatar', url); } }, [setValue] ); - const handleLoad = React.useCallback( - (id: string) => { + // 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); - getQuizListeningById(id) - .then((model: RecordModel) => { - reset({ ...defaultLpCategory, ...model }); - }) - .catch((err) => { - logger.error(err); - toast(t('dashboard.lessonTypes.list.error')); - }) - .finally(() => { - setShowLoading(false); - }); + try { + const result = await pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).getOne(id); + + reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); + setTextDescription(result.description); + setTextRemarks(result.remarks); + + if (result.cat_image !== '') { + const fetchResult = await fetch( + `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}` + ); + + const blob = await fetchResult.blob(); + const url = await fileToBase64(blob); + + setValue('avatar', url); + } else { + setValue('avatar', ''); + } + } catch (error) { + logger.error(error); + toast(t('list.error')); + + setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); + } finally { + setShowLoading(false); + } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [typeId] + [catId] ); React.useEffect(() => { - handleLoad(typeId); + void loadExistingData(catId); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [typeId]); + }, [catId]); if (showLoading) return ; + if (showError.show) + return ( + + ); return (
- } spacing={4}> + } + spacing={4} + > - {t('dashboard.lessonTypes.edit.typeInformation')} - + {t('edit.basic-info')} + - + - {/* - */} - - {t('dashboard.lessonTypes.edit.avatar')} - {t('dashboard.lessonTypes.edit.avatarRequirements')} + + {t('edit.avatar')} + {t('edit.avatarRequirements')} - + - + ( - - {t('dashboard.lessonTypes.edit.name')} + + {t('edit.cat_name')} {errors.cat_name ? {errors.cat_name.message} : null} )} /> - - ( - - {t('dashboard.lessonTypes.edit.type')} - - {errors.type ? {errors.type.message} : null} - - )} - /> - - + {/* */} + ( - - {t('dashboard.lessonTypes.edit.position')} + + {t('edit.pos')} { @@ -252,41 +327,170 @@ export function LpCategoryEditForm(): React.JSX.Element { }} type="number" /> - {errors.pos ? {errors.pos.message} : null} )} /> - + {/* */} + ( - - {t('dashboard.lessonTypes.edit.visibleToUser')} + + {t('edit.slug')} + + {errors.slug ? {errors.slug.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.visible')} - {errors.visible_to_user ? ( - {errors.visible_to_user.message} - ) : null} + {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('dashboard.lessonTypes.edit.updateButton')} + + + {t('edit.updateButton')} diff --git a/002_source/cms/src/components/dashboard/lp_categories/notifications.tsx b/002_source/cms/src/components/dashboard/lp_categories/notifications.tsx index 1adbb80..a6c16bd 100644 --- a/002_source/cms/src/components/dashboard/lp_categories/notifications.tsx +++ b/002_source/cms/src/components/dashboard/lp_categories/notifications.tsx @@ -1,7 +1,6 @@ 'use client'; import * as React from 'react'; -import { Notification } from '@/app/dashboard/Sample/Notifications/type'; import Avatar from '@mui/material/Avatar'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -13,14 +12,19 @@ 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 { useTranslation } from 'react-i18next'; -// import type { Notification } from '@/types/type'; 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 => ( @@ -61,8 +65,6 @@ export interface NotificationsProps { } export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { - const { t } = useTranslation(); - return ( } - title={t('Notifications')} + title="Notifications" />
diff --git a/002_source/cms/src/components/dashboard/lp_categories/payments.tsx b/002_source/cms/src/components/dashboard/lp_categories/payments.tsx index b57f40c..0420d32 100644 --- a/002_source/cms/src/components/dashboard/lp_categories/payments.tsx +++ b/002_source/cms/src/components/dashboard/lp_categories/payments.tsx @@ -14,7 +14,6 @@ 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 { useTranslation } from 'react-i18next'; import { dayjs } from '@/lib/dayjs'; import type { ColumnDef } from '@/components/core/data-table'; @@ -79,13 +78,12 @@ export interface PaymentsProps { } export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { - const { t } = useTranslation(); return ( }> - {t('Create Payment')} + Create Payment } avatar={ @@ -93,7 +91,7 @@ export function Payments({ ordersValue, payments = [], refundsValue, totalOrders } - title={t('Payments')} + title="Payments" /> @@ -106,13 +104,13 @@ export function Payments({ ordersValue, payments = [], refundsValue, totalOrders >
- {t('Total orders')} + Total orders {new Intl.NumberFormat('en-US').format(totalOrders)}
- {t('Orders value')} + Orders value {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} @@ -120,7 +118,7 @@ export function Payments({ ordersValue, payments = [], refundsValue, totalOrders
- {t('Refunds')} + Refunds {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} diff --git a/002_source/cms/src/components/dashboard/lp_categories/shipping-address.tsx b/002_source/cms/src/components/dashboard/lp_categories/shipping-address.tsx index ad92281..8793e5c 100644 --- a/002_source/cms/src/components/dashboard/lp_categories/shipping-address.tsx +++ b/002_source/cms/src/components/dashboard/lp_categories/shipping-address.tsx @@ -1,5 +1,3 @@ -'use client'; - import * as React from 'react'; import Button from '@mui/material/Button'; import Card from '@mui/material/Card'; @@ -8,17 +6,22 @@ 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'; -import { useTranslation } from 'react-i18next'; -import type { Address } from '@/types/Address'; +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 { - const { t } = useTranslation(); - return ( @@ -31,9 +34,9 @@ export function ShippingAddress({ address }: ShippingAddressProps): React.ReactE {address.zipCode} - {address.primary ? : } + {address.primary ? : } diff --git a/002_source/cms/src/components/dashboard/lp_categories/type.d.ts b/002_source/cms/src/components/dashboard/lp_categories/type.d.ts index 5d169b8..d044160 100644 --- a/002_source/cms/src/components/dashboard/lp_categories/type.d.ts +++ b/002_source/cms/src/components/dashboard/lp_categories/type.d.ts @@ -12,36 +12,45 @@ export interface LpCategory { lesson_id: string; description: string; remarks: string; - createdAt: Date; - visible: 'pending' | 'active' | 'blocked' | 'NA' | 'visible' | 'hidden'; + // name: string; avatar: string; email: string; phone: string; quota: number; status: 'pending' | 'active' | 'blocked' | 'NA'; - isActive: boolean; - order: number; - imageUrl: string; + createdAt: Date; } -export interface CreateForm { - name: string; - type: string; +export interface CreateFormProps { + cat_name: string; + cat_image: File[] | null; pos: number; + init_answer?: string; visible: string; - description: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: to remove + type: string; isActive: boolean; order: number; - imageUrl: string; + name?: string; + imageUrl?: string; } export interface EditFormProps { cat_name: string; + cat_image: File[] | null; pos: number; + init_answer: any; visible: string; - description?: string; + slug: string; remarks?: string; + description?: string; + // + // TODO: remove below type: string; } diff --git a/002_source/cms/src/components/dashboard/overview/summary/ActiveUserCount/index.tsx b/002_source/cms/src/components/dashboard/overview/summary/ActiveUserCount/index.tsx index e544bd7..8e4da17 100644 --- a/002_source/cms/src/components/dashboard/overview/summary/ActiveUserCount/index.tsx +++ b/002_source/cms/src/components/dashboard/overview/summary/ActiveUserCount/index.tsx @@ -1,44 +1,60 @@ 'use client'; +/* +# PROMPT + +this is a subset of a typescript project + +clone `LessonTypeCount`, `LessonCategoriesCount` to `UserCount` and do modifiy to get the count of users, thanks. +*/ import * as React from 'react'; import getAllUserMetasCount from '@/db/UserMetas/GetAllCount'; -// import GetAllUsersCount from '@/db/Users/GetAllCount.tsx'; -import { Typography } from '@mui/material'; import { Users as UsersIcon } from '@phosphor-icons/react/dist/ssr/Users'; import { useTranslation } from 'react-i18next'; import { Summary } from '@/components/dashboard/overview/summary'; +import { LoadingSummary } from '../LoadingSummary'; + function ActiveUserCount(): React.JSX.Element { const { t } = useTranslation(); const [amount, setAmount] = React.useState(0); - const [isLoading, setIsLoading] = React.useState(true); + const [showLoading, setShowLoading] = React.useState(true); + const [showError, setShowError] = React.useState(false); + const [errorDetail, setErrorDetail] = React.useState(''); React.useEffect(() => { - getAllUserMetasCount() - .then((count) => { - setAmount(count); - setIsLoading(false); - }) - .catch((err) => { - setAmount(-99); - }); + async function count(): Promise { + try { + const tempCount = await getAllUserMetasCount(); + setAmount(tempCount); + } catch (error) { + setAmount(-9); + setErrorDetail(JSON.stringify(error)); + setShowError(true); + } finally { + setShowLoading(false); + } + } + void count(); }, []); - if (isLoading) { - return Loading...; + if (showLoading) { + return ; } + if (showError) return
{errorDetail}
; + return ( ); } -export default ActiveUserCount; +export default React.memo(ActiveUserCount); diff --git a/002_source/cms/src/components/dashboard/overview/summary/LessonCategoriesCount/index.tsx b/002_source/cms/src/components/dashboard/overview/summary/LessonCategoriesCount/index.tsx index f9bb170..6993e41 100644 --- a/002_source/cms/src/components/dashboard/overview/summary/LessonCategoriesCount/index.tsx +++ b/002_source/cms/src/components/dashboard/overview/summary/LessonCategoriesCount/index.tsx @@ -8,35 +8,43 @@ this is a subset of a typescript project clone `LessonTypeCount`, `LessonCategoriesCount` to `UserCount` and do modifiy to get the count of users, thanks. */ import * as React from 'react'; -import GetAllCount from '@/db/LessonCategories/GetAllCount.tsx'; -import { Typography } from '@mui/material'; +import getAllLessonCategoriesCount from '@/db/LessonCategories/GetAllCount.tsx'; import { ListChecks as ListChecksIcon } from '@phosphor-icons/react/dist/ssr/ListChecks'; import { useTranslation } from 'react-i18next'; import { Summary } from '@/components/dashboard/overview/summary'; +import { LoadingSummary } from '../LoadingSummary'; + function LessonCategoriesCount(): React.JSX.Element { const { t } = useTranslation(); const [amount, setAmount] = React.useState(0); - const [isLoading, setIsLoading] = React.useState(true); + const [showLoading, setShowLoading] = React.useState(true); + const [showError, setShowError] = React.useState(false); + const [errorDetail, setErrorDetail] = React.useState(''); React.useEffect(() => { - GetAllCount() - .then((count) => { - setAmount(count); - }) - .catch((err) => { - // console.error(err); - }) - .finally(() => { - setIsLoading(false); - }); + async function count(): Promise { + try { + const tempCount = await getAllLessonCategoriesCount(); + setAmount(tempCount); + } catch (error) { + setAmount(-9); + setErrorDetail(JSON.stringify(error)); + setShowError(true); + } finally { + setShowLoading(false); + } + } + void count(); }, []); - if (isLoading) { - return Loading; + if (showLoading) { + return ; } + if (showError) return
{errorDetail}
; + return ( (0); const [isLoading, setIsLoading] = React.useState(true); + const [error, setError] = React.useState(null); React.useEffect(() => { - GetAllCount() - .then((count) => { + const fetchData = async () => { + try { + const count = await GetAllCount(); setAmount(count); - }) - .catch((err) => { - // console.error(err); - }) - .finally(() => { + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { setIsLoading(false); - }); + } + }; + + void fetchData(); }, []); if (isLoading) { - return Loading...; + return ; } - return ( - - ); + if (error) { + return ; + } + + return ; } export default React.memo(LessonTypeCount); diff --git a/002_source/cms/src/components/dashboard/overview/summary/LoadingSummary/index.tsx b/002_source/cms/src/components/dashboard/overview/summary/LoadingSummary/index.tsx new file mode 100644 index 0000000..c9f20b5 --- /dev/null +++ b/002_source/cms/src/components/dashboard/overview/summary/LoadingSummary/index.tsx @@ -0,0 +1,81 @@ +'use client'; + +/* +# PROMPT + +this is a subset of a typescript project + +clone `LessonTypeCount`, `LessonCategoriesCount` to `UserCount` and do modifiy to get the count of users, thanks. +*/ +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Divider from '@mui/material/Divider'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import type { Icon } from '@phosphor-icons/react/dist/lib/types'; +import { TrendDown as TrendDownIcon } from '@phosphor-icons/react/dist/ssr/TrendDown'; +import { TrendUp as TrendUpIcon } from '@phosphor-icons/react/dist/ssr/TrendUp'; +import { useTranslation } from 'react-i18next'; + +export interface SummaryProps { + diff: number; + icon: Icon; + title: string; + trend: 'up' | 'down'; +} + +export function LoadingSummary({ diff, icon: Icon, title, trend }: SummaryProps): React.JSX.Element { + const { t } = useTranslation(); + return ( + + + + + + +
+ + {title} + + Loading +
+
+
+ + + + + {trend === 'up' ? ( + + ) : ( + + )} + + + + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(diff / 100)} + {' '} + {trend === 'up' ? t('increase') : t('decrease')} {t('vs last month')} + + + +
+ ); +} diff --git a/002_source/cms/src/constants.ts b/002_source/cms/src/constants.ts index 5ac9392..f86281d 100644 --- a/002_source/cms/src/constants.ts +++ b/002_source/cms/src/constants.ts @@ -6,7 +6,10 @@ const NO_NUM = -Infinity; const NS_LESSON_CATEGORY = 'lesson_category'; const COL_USERS = 'users'; const COL_USER_METAS = 'UserMetas'; -const COL_QUIZ_LISTENINGS = 'QuizLPCategories'; + +// do not use LP_CATEGORIES +const COL_LISTENINGS_PRACTICE_CATEGORIES = 'QuizLPCategories'; +const COL_QUIZ_LP_QUESTIONS = 'QuizLPQuestions'; export { COL_LESSON_TYPES, @@ -16,6 +19,7 @@ export { NS_LESSON_CATEGORY, COL_USERS, COL_USER_METAS, - COL_QUIZ_LISTENINGS, + COL_LISTENINGS_PRACTICE_CATEGORIES, + COL_QUIZ_LP_QUESTIONS, // }; diff --git a/002_source/cms/src/db/DB_AI_GUIDELINE.MD b/002_source/cms/src/db/DB_AI_GUIDELINE.MD index b1dc3b8..39f3ee7 100644 --- a/002_source/cms/src/db/DB_AI_GUIDELINE.MD +++ b/002_source/cms/src/db/DB_AI_GUIDELINE.MD @@ -79,3 +79,8 @@ please revise `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory` to the collection `QuizLPCategories` align the dbml file in the previous prompt + + +please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx` + +to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory` diff --git a/002_source/cms/src/db/LessonCategories/Create.tsx b/002_source/cms/src/db/LessonCategories/Create.tsx index 8cd96bb..6ae24fb 100644 --- a/002_source/cms/src/db/LessonCategories/Create.tsx +++ b/002_source/cms/src/db/LessonCategories/Create.tsx @@ -2,8 +2,8 @@ import { COL_LESSON_CATEGORIES } from '@/constants'; import type { RecordModel } from 'pocketbase'; import { pb } from '@/lib/pb'; -import type { CreateForm } from '@/components/dashboard/lp_categories/type'; +import type { CreateFormProps } from '@/components/dashboard/lp_categories/type'; -export default function createLessonCategory(data: CreateForm): Promise { +export default function createLessonCategory(data: CreateFormProps): Promise { return pb.collection(COL_LESSON_CATEGORIES).create(data); } diff --git a/002_source/cms/src/db/LessonCategories/GetAllCount.tsx b/002_source/cms/src/db/LessonCategories/GetAllCount.tsx index 4f482bd..979c5bd 100644 --- a/002_source/cms/src/db/LessonCategories/GetAllCount.tsx +++ b/002_source/cms/src/db/LessonCategories/GetAllCount.tsx @@ -1,9 +1,14 @@ -// REQ0006 +// RULES: +// error handled by caller +// contain definition to collection only + import { COL_LESSON_CATEGORIES } from '@/constants'; import { pb } from '@/lib/pb'; -export default async function GetAllCount(): Promise { - const { totalItems: count } = await pb.collection(COL_LESSON_CATEGORIES).getList(1, 9999, {}); - return count; +export default function getAllLessonCategoriesCount(): Promise { + return pb + .collection(COL_LESSON_CATEGORIES) + .getList(1, 9999) + .then((res) => res.totalItems); } diff --git a/002_source/cms/src/db/LessonCategories/Update.tsx b/002_source/cms/src/db/LessonCategories/Update.tsx index afd216a..1a750e6 100644 --- a/002_source/cms/src/db/LessonCategories/Update.tsx +++ b/002_source/cms/src/db/LessonCategories/Update.tsx @@ -2,8 +2,8 @@ import { COL_LESSON_CATEGORIES } from '@/constants'; import type { RecordModel } from 'pocketbase'; import { pb } from '@/lib/pb'; -import type { CreateForm } from '@/components/dashboard/lp_categories/type'; +import type { CreateFormProps } from '@/components/dashboard/lp_categories/type'; -export default function updateLessonCategory(id: string, data: CreateForm): Promise { +export default function updateLessonCategory(id: string, data: CreateFormProps): Promise { return pb.collection(COL_LESSON_CATEGORIES).update(id, data); } diff --git a/002_source/cms/src/db/QuizListenings/Delete.tsx b/002_source/cms/src/db/QuizListenings/Delete.tsx index c90a061..1bc13fb 100644 --- a/002_source/cms/src/db/QuizListenings/Delete.tsx +++ b/002_source/cms/src/db/QuizListenings/Delete.tsx @@ -1,8 +1,8 @@ -import { COL_QUIZ_LISTENINGS } from '@/constants'; +import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import type { RecordModel } from 'pocketbase'; import { pb } from '@/lib/pb'; export default function deleteQuizListening(id: string): Promise { - return pb.collection(COL_QUIZ_LISTENINGS).delete(id); + return pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).delete(id); } diff --git a/002_source/cms/src/db/QuizListenings/GetAll.tsx b/002_source/cms/src/db/QuizListenings/GetAll.tsx index addf770..5086859 100644 --- a/002_source/cms/src/db/QuizListenings/GetAll.tsx +++ b/002_source/cms/src/db/QuizListenings/GetAll.tsx @@ -1,8 +1,8 @@ -import { COL_QUIZ_LISTENINGS } from '@/constants'; +import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import type { RecordModel } from 'pocketbase'; import { pb } from '@/lib/pb'; export default function getAllQuizListenings(): Promise { - return pb.collection(COL_QUIZ_LISTENINGS).getFullList(); + return pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).getFullList(); } diff --git a/002_source/cms/src/db/QuizListenings/GetAllCount.tsx b/002_source/cms/src/db/QuizListenings/GetAllCount.tsx index 3dc035e..7c6f6b2 100644 --- a/002_source/cms/src/db/QuizListenings/GetAllCount.tsx +++ b/002_source/cms/src/db/QuizListenings/GetAllCount.tsx @@ -1,9 +1,9 @@ // REQ0006 -import { COL_QUIZ_LISTENINGS } from '@/constants'; +import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { pb } from '@/lib/pb'; export default async function GetAllCount(): Promise { - const { totalItems: count } = await pb.collection(COL_QUIZ_LISTENINGS).getList(1, 9999, {}); + const { totalItems: count } = await pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).getList(1, 9999, {}); return count; } diff --git a/002_source/cms/src/db/QuizListenings/GetById.tsx b/002_source/cms/src/db/QuizListenings/GetById.tsx index 32dfa2f..9c7d433 100644 --- a/002_source/cms/src/db/QuizListenings/GetById.tsx +++ b/002_source/cms/src/db/QuizListenings/GetById.tsx @@ -1,8 +1,8 @@ -import { COL_QUIZ_LISTENINGS } from '@/constants'; +import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import type { RecordModel } from 'pocketbase'; import { pb } from '@/lib/pb'; export default function getQuizListeningById(id: string): Promise { - return pb.collection(COL_QUIZ_LISTENINGS).getOne(id); + return pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).getOne(id); } diff --git a/002_source/cms/src/db/QuizListenings/GetHiddenCount.tsx b/002_source/cms/src/db/QuizListenings/GetHiddenCount.tsx index 41e8772..81d9cef 100644 --- a/002_source/cms/src/db/QuizListenings/GetHiddenCount.tsx +++ b/002_source/cms/src/db/QuizListenings/GetHiddenCount.tsx @@ -1,11 +1,13 @@ // REQ0006 -import { COL_QUIZ_LISTENINGS } from '@/constants'; +import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { pb } from '@/lib/pb'; export default async function GetHiddenCount(): Promise { try { - const result = await pb.collection(COL_QUIZ_LISTENINGS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const result = await pb + .collection(COL_LISTENINGS_PRACTICE_CATEGORIES) + .getList(1, 9999, { filter: 'visible = "hidden"' }); const { totalItems: count } = result; return count; } catch (error) { diff --git a/002_source/cms/src/db/QuizListenings/GetVisibleCount.tsx b/002_source/cms/src/db/QuizListenings/GetVisibleCount.tsx index 1c4b357..b0cac5d 100644 --- a/002_source/cms/src/db/QuizListenings/GetVisibleCount.tsx +++ b/002_source/cms/src/db/QuizListenings/GetVisibleCount.tsx @@ -1,11 +1,13 @@ // REQ0006 -import { COL_QUIZ_LISTENINGS } from '@/constants'; +import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import { pb } from '@/lib/pb'; export default async function GetVisibleCount(): Promise { try { - const result = await pb.collection(COL_QUIZ_LISTENINGS).getList(1, 9999, { filter: 'visible = "visible"' }); + const result = await pb + .collection(COL_LISTENINGS_PRACTICE_CATEGORIES) + .getList(1, 9999, { filter: 'visible = "visible"' }); const { totalItems: count } = result; return count; } catch (error) { diff --git a/002_source/cms/src/db/QuizListenings/ListWithOption.tsx b/002_source/cms/src/db/QuizListenings/ListWithOption.tsx index ef5f521..543d25c 100644 --- a/002_source/cms/src/db/QuizListenings/ListWithOption.tsx +++ b/002_source/cms/src/db/QuizListenings/ListWithOption.tsx @@ -1,4 +1,4 @@ -import { COL_QUIZ_LISTENINGS } from '@/constants'; +import { COL_LISTENINGS_PRACTICE_CATEGORIES } from '@/constants'; import type { ListResult, RecordModel } from 'pocketbase'; import { pb } from '@/lib/pb'; @@ -18,5 +18,5 @@ export default function listWithOption({ rowsPerPage, listOption = {}, }: ListWithOptionParams): Promise> { - return pb.collection(COL_QUIZ_LISTENINGS).getList(currentPage + 1, rowsPerPage, listOption); + return pb.collection(COL_LISTENINGS_PRACTICE_CATEGORIES).getList(currentPage + 1, rowsPerPage, listOption); } diff --git a/002_source/cms/src/db/UserMetas/GetAllCount.tsx b/002_source/cms/src/db/UserMetas/GetAllCount.tsx index 4d36d81..bea65db 100644 --- a/002_source/cms/src/db/UserMetas/GetAllCount.tsx +++ b/002_source/cms/src/db/UserMetas/GetAllCount.tsx @@ -1,13 +1,14 @@ +// RULES: +// error handled by caller +// contain definition to collection only + import { COL_USER_METAS } from '@/constants'; import { pb } from '@/lib/pb'; -export default async function getAllUserMetasCount(): Promise { - try { - const result = await pb.collection(COL_USER_METAS).getList(1, 9998); - return result.totalItems; - } catch (error) { - console.error(error); - return -99; - } +export default function getAllUserMetasCount(): Promise { + return pb + .collection(COL_USER_METAS) + .getList(1, 9998) + .then((res) => res.totalItems); } diff --git a/002_source/cms/src/db/_PROMPT/1.MD b/002_source/cms/src/db/_PROMPT/1.MD new file mode 100644 index 0000000..2e41fed --- /dev/null +++ b/002_source/cms/src/db/_PROMPT/1.MD @@ -0,0 +1,23 @@ +Hi, please study the documentation below, +i will send you the task afterwards, + +please read and understand the documentation below and link up the ideas +reply `OK` when you done +no need to state me any other things, thanks + +1. `schema.dbml` + +- this describe the database schema in dbml format +- filepath: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml` + +2. `schema.json` + +- this is the schema export in pocketbase format +- filepath: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/schema.json` + +3. `_AI_GUIDELINE`: + +- there are the markdown files that help you better understand the implementation +- directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/_AI_GUIDELINE` + +thanks diff --git a/002_source/cms/src/db/_PROMPT/2.MD b/002_source/cms/src/db/_PROMPT/2.MD new file mode 100644 index 0000000..ca0d7b7 --- /dev/null +++ b/002_source/cms/src/db/_PROMPT/2.MD @@ -0,0 +1,35 @@ +update `LpCategoryDefaultValue` +in file `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.ts` + +thanks + +you can find the type def in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` + +please help to draft code file: + +base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db` + +using +`$base_dir/QuizListenings/GetHiddenCount.tsx`, +`$base_dir/QuizListenings/GetVisibleCount.tsx`, +`$base_dir/LessonTypes/GetHiddenCount.tsx`, +`$base_dir/LessonTypes/GetVisibleCount.tsx`, +as reference, + +look into the all directories under base_dir e.g. `QuizCategories`. +propergate `GetHiddenCount.tsx` and `GetVisibleCount.tsx` if missing, do the change to suit the collection. +use `.draft.tsx` instead when you write file + +--- + +rewrite `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/LessonCategories/GetAllCount.tsx` to match `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/UserMetas/GetAllCount.tsx` style + +--- + +style rewrite + +study +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/overview/summary/ActiveUserCount/index.tsx` +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/overview/summary/LessonCategoriesCount/index.tsx` + +and rewrite `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/overview/summary/LessonTypeCount/index.tsx` to match style above thanks diff --git a/002_source/cms/src/db/_PROMPT/3.MD b/002_source/cms/src/db/_PROMPT/3.MD new file mode 100644 index 0000000..6b3d11d --- /dev/null +++ b/002_source/cms/src/db/_PROMPT/3.MD @@ -0,0 +1,47 @@ +please draft with idea: + +``` +await pb +.collection(COL_LESSON_TYPES) +.getList(currentPage + 1, rowsPerPage, listOption); +``` + +for Listening Practice + +thanks + +I want you to clone +from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/LessonTypes/GetVisibleCount.tsx` (source file) + +to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizListenings/GetVisibleCount.tsx` (dest file) + +please extract , link up and remember the document properties +(e.g. types, functions, variables, constants, etc) +from source file +draft dest file + +update the variables and properties of dest file to reflect `listening practice categories`/`lp_categories` + +--- + +## task + +update `schema.dbml` to reflect `schema.json` + +## details + +Hi, +I have a pocketbase export json file: +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/pocketbase/pb_hooks/seed/schema.json` + +and a dbml file: +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml` + +the collection name in pocketbase should be reflected by a table in dbml, + +## steps + +compare `schema.json` and `schema.dbml` +please keep `schema.json` remain unchanged +update `schema.dbml` to reflect `schema.json` +do check again when finished diff --git a/002_source/cms/src/db/_PROMPT/4.md b/002_source/cms/src/db/_PROMPT/4.md new file mode 100644 index 0000000..e08d27a --- /dev/null +++ b/002_source/cms/src/db/_PROMPT/4.md @@ -0,0 +1,57 @@ +--- + +clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it + +please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden` + +well done !, please proceed to another request + +working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db` + +according information from `schema.json`, get the collection of `Students` + +pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content + +when you draft coding, review file and append with `.tsx.draft` + +--- + +- this is part of react typescript project, with pocketbase +- `schema.dbml`, describe the collections(tables) +- folder `LessonCategories`, the correct references +- folder `LessonTypes`, the correct references +- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006` +- do not read root directory, assume it is a fresh copy of nextjs project is ok + +## instruction + +- break the questions into smaller parts +- review file append with `.draft`, see if the content aligned with the correct references +- read and understand `dbml` file +- lookup the every folder + +## tasks + +Thanks + +--- + +please take a look in `schema.dbml` and `schema.json`, +associate the collection from json file to the table in dbml file + +please modify the `schema.dbml` to align with `schema.json` + +to the collection `QuizLPCategories` align the dbml file in the previous prompt + +--- + +please revise + +please revise +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory` + +to the collection `QuizLPCategories` align the dbml file in the previous prompt + +please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx` + +to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory` diff --git a/002_source/cms/src/db/_PROMPT/temp.md b/002_source/cms/src/db/_PROMPT/temp.md new file mode 100644 index 0000000..c008f7d --- /dev/null +++ b/002_source/cms/src/db/_PROMPT/temp.md @@ -0,0 +1,6 @@ +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/lp-categories-filters.tsx` + +this file is original for `lesson_category` model, +please modify it to fit `lp_category` (listening practice category) + +thanks diff --git a/002_source/cms/src/db/schema.json b/002_source/cms/src/db/schema.json index 296e20f..50aa4ef 100644 --- a/002_source/cms/src/db/schema.json +++ b/002_source/cms/src/db/schema.json @@ -1356,6 +1356,59 @@ "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" + }, + { + "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" } ], "indexes": [], @@ -2648,4 +2701,4 @@ "indexes": [], "system": false } -] \ No newline at end of file +] diff --git a/002_source/cms/src/lib/file-to-base64.tsx b/002_source/cms/src/lib/file-to-base64.tsx index b94321a..85d37a6 100644 --- a/002_source/cms/src/lib/file-to-base64.tsx +++ b/002_source/cms/src/lib/file-to-base64.tsx @@ -10,3 +10,20 @@ export function fileToBase64(file: Blob): Promise { }; }); } + +export function base64ToFile(base64String: string, filename?: string): Promise { + return new Promise((resolve, reject) => { + const arr = base64String.split(','); + const type = arr[0].match(/:(.*?);/)![1]; + const bstr = atob(arr[1]); + let n = bstr.length; + const u8arr = new Uint8Array(n); + + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + + const file = new File([u8arr], filename || 'file', { type }); + resolve(file); + }); +}