diff --git a/002_source/cms/src/app/dashboard/Sample/repomix-output.xml b/002_source/cms/src/app/dashboard/Sample/repomix-output.xml new file mode 100644 index 0000000..c1ddc78 --- /dev/null +++ b/002_source/cms/src/app/dashboard/Sample/repomix-output.xml @@ -0,0 +1,558 @@ +This file is a merged representation of the entire codebase, combined into a single document by Repomix. + + +This section contains a summary of this file. + + +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + + + +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Repository files, each consisting of: + - File path as an attribute + - Full contents of the file + + + +- This file should be treated as read-only. Any changes should be made to the + original repository files, not this packed version. +- When processing this file, use the file path to distinguish + between different files in the repository. +- Be aware that this file may contain sensitive information. Handle it with + the same level of security as you would the original repository. + + + +- Some files may have been excluded based on .gitignore rules and Repomix's configuration +- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files +- Files matching patterns in .gitignore are excluded +- Files matching default ignore patterns are excluded +- Files are sorted by Git change count (files with more changes are at the bottom) + + + + + + + + + +AddressCard/ + index.tsx + SampleAddresses.tsx +BasicDetailCard/ + index.tsx +Notifications/ + index.tsx + type.d.ts +PaymentCard/ + index.tsx + SamplePayments.tsx +SecurityCard/ + index.tsx +TitleCard/ + index.tsx +Helloworld.tsx + + + +This section contains the contents of the repository's files. + + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Grid from '@mui/material/Unstable_Grid2'; +import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { useTranslation } from 'react-i18next'; + +import type { Address } from '@/types/Address'; +import { ShippingAddress } from '@/components/dashboard/lp/categories/shipping-address'; + +import { SampleAddresses } from './SampleAddresses'; + +export default function SampleAddressCard(): React.JSX.Element { + const { t } = useTranslation(); + return ( + + } + > + {t('list.add')} + + } + avatar={ + + + + } + title={t('list.shipping-addresses')} + /> + + + {(SampleAddresses satisfies Address[]).map((address) => ( + + + + ))} + + + + ); +} + + + +'use client'; + +import type { Address } from '@/types/Address'; + +export const SampleAddresses: Address[] = [ + { + id: 'ADR-001', + country: 'United States', + state: 'Michigan', + city: 'Lansing', + zipCode: '48933', + street: '480 Haven Lane', + primary: true, + }, + { + id: 'ADR-002', + country: 'United States', + state: 'Missouri', + city: 'Springfield', + zipCode: '65804', + street: '4807 Lighthouse Drive', + }, +]; + + + +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +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 LinearProgress from '@mui/material/LinearProgress'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +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'; + +export default function BasicDetailCard({ + lpCatId, + handleEditClick, +}: { + lpCatId: string; + 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: 'Miron Vitold' }, + { key: 'Email', value: 'miron.vitold@domain.com' }, + { key: 'Phone', value: '(425) 434-5535' }, + { key: 'Company', value: 'Devias IO' }, + { + key: 'Quota', + value: ( + + + + 50% + + + ), + }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + ); +} + + + +'use client'; + +import { dayjs } from '@/lib/dayjs'; + +import type { Notification } from './type'; + +export const SampleNotifications: Notification[] = [ + { + id: 'EV-002', + type: 'Refund request approved', + status: 'pending', + createdAt: dayjs().subtract(34, 'minute').subtract(5, 'hour').subtract(3, 'day').toDate(), + }, + { + id: 'EV-001', + type: 'Order confirmation', + status: 'delivered', + createdAt: dayjs().subtract(49, 'minute').subtract(11, 'hour').subtract(4, 'day').toDate(), + }, +]; + + + +export interface Notification { + id: string; + type: string; + status: 'delivered' | 'pending' | 'failed'; + createdAt: Date; +} + + + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Divider from '@mui/material/Divider'; +import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { useTranslation } from 'react-i18next'; + +import { PropertyItem } from '@/components/core/property-item'; +import { PropertyList } from '@/components/core/property-list'; +import { Payments } from '@/components/dashboard/lp/categories/payments'; + +import { SamplePayments } from './SamplePayments'; + +export default function SamplePaymentCard(): React.JSX.Element { + const { t } = useTranslation(); + return ( + <> + + + } + > + {t('list.edit')} + + } + avatar={ + + + + } + title={t('list.billing-details')} + /> + + + } + sx={{ '--PropertyItem-padding': '16px' }} + > + {( + [ + { key: t('Credit card'), value: '**** 4142' }, + { key: t('Country'), value: t('United States') }, + { key: t('State'), value: t('Michigan') }, + { key: t('City'), value: t('Southfield') }, + { key: t('Address'), value: t('Address') }, + { key: t('Tax ID'), value: t('Tax ID') }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + + + + ); +} + + + +'use client'; + +// import { dayjs } from 'dayjs'; +import type { Payment } from '@/types/Payment'; +import { dayjs } from '@/lib/dayjs'; + +export const SamplePayments: Payment[] = [ + { + currency: 'USD', + amount: 500, + invoiceId: 'INV-005', + status: 'completed', + createdAt: dayjs().subtract(5, 'minute').subtract(1, 'hour').toDate(), + }, + { + currency: 'USD', + amount: 324.5, + invoiceId: 'INV-004', + status: 'refunded', + createdAt: dayjs().subtract(21, 'minute').subtract(2, 'hour').toDate(), + }, + { + currency: 'USD', + amount: 746.5, + invoiceId: 'INV-003', + status: 'completed', + createdAt: dayjs().subtract(7, 'minute').subtract(3, 'hour').toDate(), + }, + { + currency: 'USD', + amount: 56.89, + invoiceId: 'INV-002', + status: 'completed', + createdAt: dayjs().subtract(48, 'minute').subtract(4, 'hour').toDate(), + }, + { + currency: 'USD', + amount: 541.59, + invoiceId: 'INV-001', + status: 'completed', + createdAt: dayjs().subtract(31, 'minute').subtract(5, 'hour').toDate(), + }, +]; + + + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning'; +import { useTranslation } from 'react-i18next'; + +export default function SampleSecurityCard(): React.JSX.Element { + const { t } = useTranslation(); + + return ( + + + + + } + title={t('list.security')} + /> + + +
+ +
+ + {t('a-deleted-customer-cannot-be-restored-all-data-will-be-permanently-removed')} + +
+
+
+ ); +} +
+ + +'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'; + +export default function SampleTitleCard(): React.JSX.Element { + const { t } = useTranslation(); + + return ( + <> + + + empty + +
+ + {t('list.customer-name')} + + } + label={t('list.active')} + size="small" + variant="outlined" + /> + + + {t('list.customer-email')} + +
+
+
+ +
+ + ); +} +
+ + +'use client'; + +import * as React from 'react'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; + +function Page(): React.JSX.Element { + React.useLayoutEffect(() => { + console.log('helloworld'); + }, []); + + return <>helloworld; +} + +export default Page; + + +
diff --git a/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/BasicDetailCard.tsx b/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/BasicDetailCard.tsx index 6683056..e79c927 100644 --- a/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/BasicDetailCard.tsx +++ b/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/BasicDetailCard.tsx @@ -13,13 +13,13 @@ import { useTranslation } from 'react-i18next'; import { PropertyItem } from '@/components/core/property-item'; import { PropertyList } from '@/components/core/property-list'; -import { LpCategory } from '@/components/dashboard/lp/categories/type'; +import { CrCategory } from '@/components/dashboard/cr/categories/type'; export default function BasicDetailCard({ lpModel: model, handleEditClick, }: { - lpModel: LpCategory; + lpModel: CrCategory; handleEditClick: () => void; }): React.JSX.Element { const { t } = useTranslation(); diff --git a/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/TitleCard.tsx b/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/TitleCard.tsx index 6e38fb6..9994060 100644 --- a/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/TitleCard.tsx +++ b/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/TitleCard.tsx @@ -10,13 +10,13 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { useTranslation } from 'react-i18next'; -import { LpCategory } from '@/components/dashboard/lp/categories/type'; +import type { CrCategory } from '@/components/dashboard/cr/categories/type'; -function getImageUrlFrRecord(record: LpCategory): string { +function getImageUrlFrRecord(record: CrCategory): string { return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; } -export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element { +export default function SampleTitleCard({ lpModel }: { lpModel: CrCategory }): React.JSX.Element { const { t } = useTranslation(); return ( diff --git a/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/page.tsx index c25de18..d159c40 100644 --- a/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/cr/categories/[cat_id]/page.tsx @@ -7,7 +7,7 @@ import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; -import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; import Box from '@mui/material/Box'; import Link from '@mui/material/Link'; import Stack from '@mui/material/Stack'; @@ -21,9 +21,9 @@ import { logger } from '@/lib/default-logger'; import { pb } from '@/lib/pb'; import { toast } from '@/components/core/toaster'; import ErrorDisplay from '@/components/dashboard/error'; -import { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants.ts'; -import { Notifications } from '@/components/dashboard/lp/categories/notifications'; -import type { LpCategory } from '@/components/dashboard/lp/categories/type'; +import { defaultCrCategory } from '@/components/dashboard/cr/categories/_constants.ts'; +import { Notifications } from '@/components/dashboard/cr/categories/notifications'; +import type { CrCategory } from '@/components/dashboard/cr/categories/type'; import FormLoading from '@/components/loading'; import BasicDetailCard from './BasicDetailCard'; @@ -39,18 +39,18 @@ export default function Page(): React.JSX.Element { const [showError, setShowError] = React.useState({ show: false, detail: '' }); // - const [showLessonCategory, setShowLessonCategory] = React.useState(defaultLpCategory); + const [showLessonCategory, setShowLessonCategory] = React.useState(defaultCrCategory); function handleEditClick() { - router.push(paths.dashboard.lp_categories.edit(showLessonCategory.id)); + router.push(paths.dashboard.cr_categories.edit(showLessonCategory.id)); } React.useEffect(() => { if (catId) { - pb.collection(COL_QUIZ_LP_CATEGORIES) + pb.collection(COL_QUIZ_CR_CATEGORIES) .getOne(catId) .then((model: RecordModel) => { - setShowLessonCategory({ ...defaultLpCategory, ...model }); + setShowLessonCategory({ ...defaultCrCategory, ...model }); }) .catch((err) => { logger.error(err); @@ -89,7 +89,7 @@ export default function Page(): React.JSX.Element { diff --git a/002_source/cms/src/app/dashboard/cr/categories/create/page.tsx b/002_source/cms/src/app/dashboard/cr/categories/create/page.tsx index 4bc1776..f3915f0 100644 --- a/002_source/cms/src/app/dashboard/cr/categories/create/page.tsx +++ b/002_source/cms/src/app/dashboard/cr/categories/create/page.tsx @@ -13,10 +13,10 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; -import { LpCategoryCreateForm } from '@/components/dashboard/lp/categories/lp-category-create-form'; +import { CrCategoryCreateForm } from '@/components/dashboard/cr/categories/cr-category-create-form'; export default function Page(): React.JSX.Element { - // RULES: follow the name of page directory + // RULES: follow the name of page directory e.g. cr/categoies -> cr_categories const { t } = useTranslation(['lp_categories']); return ( @@ -34,7 +34,7 @@ export default function Page(): React.JSX.Element { @@ -46,7 +46,7 @@ export default function Page(): React.JSX.Element { {t('create.title')} - + ); diff --git a/002_source/cms/src/app/dashboard/cr/categories/edit/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/cr/categories/edit/[cat_id]/page.tsx index 149a75b..356139c 100644 --- a/002_source/cms/src/app/dashboard/cr/categories/edit/[cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/cr/categories/edit/[cat_id]/page.tsx @@ -10,7 +10,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; -import { LpCategoryEditForm } from '@/components/dashboard/lp/categories/lp-category-edit-form'; +import { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form'; export default function Page(): React.JSX.Element { const { t } = useTranslation(['lp_categories']); @@ -34,7 +34,7 @@ export default function Page(): React.JSX.Element { @@ -46,7 +46,7 @@ export default function Page(): React.JSX.Element { {t('edit.title')} - + ); diff --git a/002_source/cms/src/app/dashboard/cr/categories/page.tsx b/002_source/cms/src/app/dashboard/cr/categories/page.tsx index 93bb730..53cea07 100644 --- a/002_source/cms/src/app/dashboard/cr/categories/page.tsx +++ b/002_source/cms/src/app/dashboard/cr/categories/page.tsx @@ -1,12 +1,12 @@ 'use client'; // RULES: -// contains list page for lp_categories (QuizLPCategories) +// contains list page for cr_categories (QuizCRCategories) // contain definition to collection only // import * as React from 'react'; import { useRouter } from 'next/navigation'; -import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; import { LoadingButton } from '@mui/lab'; import Box from '@mui/material/Box'; import Card from '@mui/material/Card'; @@ -22,21 +22,21 @@ import isDevelopment from '@/lib/check-is-development'; import { logger } from '@/lib/default-logger'; import { pb } from '@/lib/pb'; import { toast } from '@/components/core/toaster'; +import { defaultCrCategory } from '@/components/dashboard/cr/categories/_constants'; +import { CrCategoriesFilters } from '@/components/dashboard/cr/categories/cr-categories-filters'; +import type { Filters } from '@/components/dashboard/cr/categories/cr-categories-filters'; +import { CrCategoriesPagination } from '@/components/dashboard/cr/categories/cr-categories-pagination'; +import { CrCategoriesSelectionProvider } from '@/components/dashboard/cr/categories/cr-categories-selection-context'; +import { CrCategoriesTable } from '@/components/dashboard/cr/categories/cr-categories-table'; +import type { CrCategory } from '@/components/dashboard/cr/categories/type'; import ErrorDisplay from '@/components/dashboard/error'; -import { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants'; -import { LpCategoriesFilters } from '@/components/dashboard/lp/categories/lp-categories-filters'; -import type { Filters } from '@/components/dashboard/lp/categories/lp-categories-filters'; -import { LpCategoriesPagination } from '@/components/dashboard/lp/categories/lp-categories-pagination'; -import { LpCategoriesSelectionProvider } from '@/components/dashboard/lp/categories/lp-categories-selection-context'; -import { LpCategoriesTable } from '@/components/dashboard/lp/categories/lp-categories-table'; -import type { LpCategory } from '@/components/dashboard/lp/categories/type'; import FormLoading from '@/components/loading'; export default function Page({ searchParams }: PageProps): React.JSX.Element { const { t } = useTranslation(['lp_categories']); const { email, phone, sortDir, status, name, visible, type } = searchParams; const router = useRouter(); - const [lessonCategoriesData, setLessonCategoriesData] = React.useState([]); + const [lessonCategoriesData, setLessonCategoriesData] = React.useState([]); // const [isLoadingAddPage, setIsLoadingAddPage] = React.useState(false); @@ -45,7 +45,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { // const [rowsPerPage, setRowsPerPage] = React.useState(5); // - const [f, setF] = React.useState([]); + const [f, setF] = React.useState([]); const [currentPage, setCurrentPage] = React.useState(0); // const [recordCount, setRecordCount] = React.useState(0); @@ -60,11 +60,11 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { const reloadRows = async (): Promise => { try { const models: ListResult = await pb - .collection(COL_QUIZ_LP_CATEGORIES) + .collection(COL_QUIZ_CR_CATEGORIES) .getList(currentPage + 1, rowsPerPage, listOption); const { items, totalItems } = models; - const tempLessonTypes: LpCategory[] = items.map((lt) => { - return { ...defaultLpCategory, ...lt }; + const tempLessonTypes: CrCategory[] = items.map((lt) => { + return { ...defaultCrCategory, ...lt }; }); setLessonCategoriesData(tempLessonTypes); @@ -172,7 +172,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { loading={isLoadingAddPage} onClick={(): void => { setIsLoadingAddPage(true); - router.push(paths.dashboard.lp_categories.create); + router.push(paths.dashboard.cr_categories.create); }} startIcon={} variant="contained" @@ -181,22 +181,22 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { - + - - - - +
{JSON.stringify(f, null, 2)}
@@ -215,7 +215,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { // Sorting and filtering has to be done on the server. -function applySort(row: LpCategory[], sortDir: 'asc' | 'desc' | undefined): LpCategory[] { +function applySort(row: CrCategory[], sortDir: 'asc' | 'desc' | undefined): CrCategory[] { return row.sort((a, b) => { if (sortDir === 'asc') { return a.createdAt.getTime() - b.createdAt.getTime(); @@ -225,7 +225,7 @@ function applySort(row: LpCategory[], sortDir: 'asc' | 'desc' | undefined): LpCa }); } -function applyFilters(row: LpCategory[], { email, phone, status, name, visible }: Filters): LpCategory[] { +function applyFilters(row: CrCategory[], { email, phone, status, name, visible }: Filters): CrCategory[] { return row.filter((item) => { if (email) { if (!item.email?.toLowerCase().includes(email.toLowerCase())) { diff --git a/002_source/cms/src/app/dashboard/cr/questions/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/cr/questions/[cat_id]/page.tsx index 08488f3..dd0d7eb 100644 --- a/002_source/cms/src/app/dashboard/cr/questions/[cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/cr/questions/[cat_id]/page.tsx @@ -7,7 +7,7 @@ import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; -import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; import { Grid } from '@mui/material'; import Box from '@mui/material/Box'; import Link from '@mui/material/Link'; @@ -21,9 +21,9 @@ import { logger } from '@/lib/default-logger'; import { pb } from '@/lib/pb'; import { toast } from '@/components/core/toaster'; import ErrorDisplay from '@/components/dashboard/error'; -import { defaultLpQuestion } from '@/components/dashboard/lp/questions/_constants.ts'; -import { Notifications } from '@/components/dashboard/lp/questions/notifications'; -import type { LpQuestion } from '@/components/dashboard/lp/questions/type'; +import { defaultCrQuestion } from '@/components/dashboard/cr/questions/_constants.ts'; +import { Notifications } from '@/components/dashboard/cr/questions/notifications'; +import type { CrQuestion } from '@/components/dashboard/cr/questions/type'; import FormLoading from '@/components/loading'; import BasicDetailCard from './BasicDetailCard'; @@ -39,18 +39,18 @@ export default function Page(): React.JSX.Element { const [showError, setShowError] = React.useState({ show: false, detail: '' }); // - const [showLessonQuestion, setShowLessonQuestion] = React.useState(defaultLpQuestion); + const [showLessonQuestion, setShowLessonQuestion] = React.useState(defaultCrQuestion); function handleEditClick() { - router.push(paths.dashboard.lp_questions.edit(showLessonQuestion.id)); + router.push(paths.dashboard.cr_questions.edit(showLessonQuestion.id)); } React.useEffect(() => { if (catId) { - pb.collection(COL_QUIZ_LP_QUESTIONS) + pb.collection(COL_QUIZ_CR_QUESTIONS) .getOne(catId) .then((model: RecordModel) => { - setShowLessonQuestion({ ...defaultLpQuestion, ...model }); + setShowLessonQuestion({ ...defaultCrQuestion, ...model }); }) .catch((err) => { logger.error(err); @@ -89,7 +89,7 @@ export default function Page(): React.JSX.Element { diff --git a/002_source/cms/src/app/dashboard/cr/questions/lp-categories-sample-data.tsx b/002_source/cms/src/app/dashboard/cr/questions/cr-categories-sample-data.tsx similarity index 98% rename from 002_source/cms/src/app/dashboard/cr/questions/lp-categories-sample-data.tsx rename to 002_source/cms/src/app/dashboard/cr/questions/cr-categories-sample-data.tsx index 0c1aa79..6a9b57e 100644 --- a/002_source/cms/src/app/dashboard/cr/questions/lp-categories-sample-data.tsx +++ b/002_source/cms/src/app/dashboard/cr/questions/cr-categories-sample-data.tsx @@ -1,7 +1,7 @@ import { dayjs } from '@/lib/dayjs'; import { LessonCategory } from '@/components/dashboard/lesson_category/type'; -export const LpCategoriesSampleData = [ +export const CrCategoriesSampleData = [ { id: 'USR-005', name: 'Fran Perez', diff --git a/002_source/cms/src/app/dashboard/cr/questions/create/page.tsx b/002_source/cms/src/app/dashboard/cr/questions/create/page.tsx index ff3c9dc..2958f9a 100644 --- a/002_source/cms/src/app/dashboard/cr/questions/create/page.tsx +++ b/002_source/cms/src/app/dashboard/cr/questions/create/page.tsx @@ -13,7 +13,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; -import { LpQuestionCreateForm } from '@/components/dashboard/lp/questions/lp-question-create-form'; +import { CrQuestionCreateForm } from '@/components/dashboard/cr/questions/cr-question-create-form'; export default function Page(): React.JSX.Element { // RULES: follow the name of page directory @@ -46,7 +46,7 @@ export default function Page(): React.JSX.Element { {t('create.title')} - +
); diff --git a/002_source/cms/src/app/dashboard/cr/questions/edit/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/cr/questions/edit/[cat_id]/page.tsx index 321b5a9..194b53f 100644 --- a/002_source/cms/src/app/dashboard/cr/questions/edit/[cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/cr/questions/edit/[cat_id]/page.tsx @@ -10,7 +10,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; -import { LpQuestionEditForm } from '@/components/dashboard/lp/questions/lp-question-edit-form'; +import { CrQuestionEditForm } from '@/components/dashboard/cr/questions/cr-question-edit-form'; export default function Page(): React.JSX.Element { const { t } = useTranslation(['lp_questions']); @@ -46,7 +46,7 @@ export default function Page(): React.JSX.Element { {t('edit.title')} - + ); diff --git a/002_source/cms/src/app/dashboard/cr/questions/page.tsx b/002_source/cms/src/app/dashboard/cr/questions/page.tsx index 062b71d..6c282e2 100644 --- a/002_source/cms/src/app/dashboard/cr/questions/page.tsx +++ b/002_source/cms/src/app/dashboard/cr/questions/page.tsx @@ -1,12 +1,12 @@ 'use client'; // RULES: -// contains list page for lp_questions (QuizLPQuestions) +// contains list page for cr_questions (QuizCRQuestions) // contain definition to collection only // import * as React from 'react'; import { useRouter } from 'next/navigation'; -import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; import { LoadingButton } from '@mui/lab'; import Box from '@mui/material/Box'; import Card from '@mui/material/Card'; @@ -23,20 +23,20 @@ import { logger } from '@/lib/default-logger'; import { pb } from '@/lib/pb'; import { toast } from '@/components/core/toaster'; import ErrorDisplay from '@/components/dashboard/error'; -import { defaultLpQuestion } from '@/components/dashboard/lp/questions/_constants'; -import { LpQuestionsFilters } from '@/components/dashboard/lp/questions/lp-questions-filters'; -import type { Filters } from '@/components/dashboard/lp/questions/lp-questions-filters'; -import { LpQuestionsPagination } from '@/components/dashboard/lp/questions/lp-questions-pagination'; -import { LpQuestionsSelectionProvider } from '@/components/dashboard/lp/questions/lp-questions-selection-context'; -import { LpQuestionsTable } from '@/components/dashboard/lp/questions/lp-questions-table'; -import type { LpQuestion } from '@/components/dashboard/lp/questions/type'; +import { defaultCrQuestion } from '@/components/dashboard/cr/questions/_constants'; +import { CrQuestionsFilters } from '@/components/dashboard/cr/questions/cr-questions-filters'; +import type { Filters } from '@/components/dashboard/cr/questions/cr-questions-filters'; +import { CrQuestionsPagination } from '@/components/dashboard/cr/questions/cr-questions-pagination'; +import { CrQuestionsSelectionProvider } from '@/components/dashboard/cr/questions/cr-questions-selection-context'; +import { CrQuestionsTable } from '@/components/dashboard/cr/questions/cr-questions-table'; +import type { CrQuestion } from '@/components/dashboard/cr/questions/type'; import FormLoading from '@/components/loading'; export default function Page({ searchParams }: PageProps): React.JSX.Element { const { t } = useTranslation(['lp_questions']); const { email, phone, sortDir, status, name, visible, type } = searchParams; const router = useRouter(); - const [lessonQuestionsData, setLessonCategoriesData] = React.useState([]); + const [lessonQuestionsData, setLessonCategoriesData] = React.useState([]); // const [isLoadingAddPage, setIsLoadingAddPage] = React.useState(false); @@ -45,7 +45,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { // const [rowsPerPage, setRowsPerPage] = React.useState(5); // - const [f, setF] = React.useState([]); + const [f, setF] = React.useState([]); const [currentPage, setCurrentPage] = React.useState(0); // const [recordCount, setRecordCount] = React.useState(0); @@ -59,11 +59,11 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { const reloadRows = async (): Promise => { try { const models: ListResult = await pb - .collection(COL_QUIZ_LP_QUESTIONS) + .collection(COL_QUIZ_CR_QUESTIONS) .getList(currentPage + 1, rowsPerPage, listOption); const { items, totalItems } = models; - const tempLessonTypes: LpQuestion[] = items.map((lt) => { - return { ...defaultLpQuestion, ...lt }; + const tempLessonTypes: CrQuestion[] = items.map((lt) => { + return { ...defaultCrQuestion, ...lt }; }); setLessonCategoriesData(tempLessonTypes); @@ -88,15 +88,13 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { React.useEffect(() => { if (!isFirstRun.current) { isFirstRun.current = true; + } else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { + // reset page number as tab changes + setLastListOption(listOption); + setCurrentPage(0); + void reloadRows(); } else { - if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { - // reset page number as tab changes - setLastListOption(listOption); - setCurrentPage(0); - void reloadRows(); - } else { - void reloadRows(); - } + void reloadRows(); } }, [currentPage, rowsPerPage, listOption]); @@ -171,7 +169,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { loading={isLoadingAddPage} onClick={(): void => { setIsLoadingAddPage(true); - router.push(paths.dashboard.lp_questions.create); + router.push(paths.dashboard.cr_questions.create); }} startIcon={} variant="contained" @@ -180,22 +178,22 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { - + - - - - +
{JSON.stringify(f, null, 2)}
@@ -214,7 +212,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { // Sorting and filtering has to be done on the server. -function applySort(row: LpQuestion[], sortDir: 'asc' | 'desc' | undefined): LpQuestion[] { +function applySort(row: CrQuestion[], sortDir: 'asc' | 'desc' | undefined): CrQuestion[] { return row.sort((a, b) => { if (sortDir === 'asc') { return a.createdAt.getTime() - b.createdAt.getTime(); @@ -224,7 +222,7 @@ function applySort(row: LpQuestion[], sortDir: 'asc' | 'desc' | undefined): LpQu }); } -function applyFilters(row: LpQuestion[], { email, phone, status, name, visible }: Filters): LpQuestion[] { +function applyFilters(row: CrQuestion[], { email, phone, status, name, visible }: Filters): CrQuestion[] { return row.filter((item) => { if (email) { if (!item.email?.toLowerCase().includes(email.toLowerCase())) { diff --git a/002_source/cms/src/app/dashboard/cr/repomix-output.xml b/002_source/cms/src/app/dashboard/cr/repomix-output.xml deleted file mode 100644 index c11e03d..0000000 --- a/002_source/cms/src/app/dashboard/cr/repomix-output.xml +++ /dev/null @@ -1,1783 +0,0 @@ -This file is a merged representation of the entire codebase, combined into a single document by Repomix. - - -This section contains a summary of this file. - - -This file contains a packed representation of the entire repository's contents. -It is designed to be easily consumable by AI systems for analysis, code review, -or other automated processes. - - - -The content is organized as follows: -1. This summary section -2. Repository information -3. Directory structure -4. Repository files, each consisting of: - - File path as an attribute - - Full contents of the file - - - -- This file should be treated as read-only. Any changes should be made to the - original repository files, not this packed version. -- When processing this file, use the file path to distinguish - between different files in the repository. -- Be aware that this file may contain sensitive information. Handle it with - the same level of security as you would the original repository. - - - -- Some files may have been excluded based on .gitignore rules and Repomix's configuration -- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files -- Files matching patterns in .gitignore are excluded -- Files matching default ignore patterns are excluded -- Files are sorted by Git change count (files with more changes are at the bottom) - - - - - - - - - -categories/ - [cat_id]/ - BasicDetailCard.tsx - page.tsx - TitleCard.tsx - create/ - page copy 2.tsx - page copy.tsx - page.tsx - edit/ - [cat_id]/ - _PROMPT.md - page copy.tsx - page.tsx - lp-categories-sample-data.tsx - page.tsx -questions/ - [cat_id]/ - BasicDetailCard.tsx - page.tsx - TitleCard.tsx - create/ - page.tsx - edit/ - [cat_id]/ - _PROMPT.md - page.tsx - lp-categories-sample-data.tsx - page.tsx - - - -This section contains the contents of the repository's files. - - -'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 => ( - - ) - )} - - - ); -} - - - -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; -import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; -import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; -import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; -import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Grid from '@mui/material/Unstable_Grid2'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import type { RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { pb } from '@/lib/pb'; -import { toast } from '@/components/core/toaster'; -import ErrorDisplay from '@/components/dashboard/error'; -import { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants.ts'; -import { Notifications } from '@/components/dashboard/lp/categories/notifications'; -import type { LpCategory } from '@/components/dashboard/lp/categories/type'; -import FormLoading from '@/components/loading'; - -import BasicDetailCard from './BasicDetailCard'; -import TitleCard from './TitleCard'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(); - const router = useRouter(); - // - const { cat_id: catId } = useParams<{ cat_id: string }>(); - // - const [showLoading, setShowLoading] = React.useState(true); - const [showError, setShowError] = React.useState({ show: false, detail: '' }); - - // - const [showLessonCategory, setShowLessonCategory] = React.useState(defaultLpCategory); - - function handleEditClick() { - router.push(paths.dashboard.lp_categories.edit(showLessonCategory.id)); - } - - React.useEffect(() => { - if (catId) { - pb.collection(COL_QUIZ_LP_CATEGORIES) - .getOne(catId) - .then((model: RecordModel) => { - setShowLessonCategory({ ...defaultLpCategory, ...model }); - }) - .catch((err) => { - logger.error(err); - toast(t('list.error')); - - setShowError({ show: true, detail: JSON.stringify(err) }); - }) - .finally(() => { - setShowLoading(false); - }); - } - }, [catId]); - - if (showLoading) return ; - if (showError.show) - return ( - - ); - - return ( - - - -
- - - {t('list.title')} - -
- - - -
- - - - - - - - - - - - - - - -
-
- ); -} -
- - -'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: LpCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} - -export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element { - const { t } = useTranslation(); - - return ( - <> - - - {t('empty')} - -
- - {lpModel.cat_name} - - } - label={lpModel.visible} - size="small" - variant="outlined" - /> - - - {lpModel.slug} - -
-
-
- -
- - ); -} -
- - -'use client'; - -// RULES: -// T.B.A. -// -import * as React from 'react'; -import RouterLink from 'next/link'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { LpCategoryCreateForm } from '@/components/dashboard/lp/categories/lp-category-create-form'; - -export default function Page(): React.JSX.Element { - // RULES: follow the name of page directory - const { t } = useTranslation(['lp_categories']); - - return ( - - - -
- - - {t('title')} - -
-
- {t('create.title')} -
-
- -
-
- ); -} -
- - -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { LessonCategoryCreateForm } from '@/components/dashboard/lesson_category/lesson-category-create-form'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(); - return ( - - - -
- - - {t('title', { ns: 'lesson_category' })} - -
-
- {t('create.title', { ns: 'lesson_category' })} -
-
- -
-
- ); -} -
- - -'use client'; - -// RULES: -// T.B.A. -// -import * as React from 'react'; -import RouterLink from 'next/link'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { LpCategoryCreateForm } from '@/components/dashboard/lp/categories/lp-category-create-form'; - -export default function Page(): React.JSX.Element { - // RULES: follow the name of page directory - const { t } = useTranslation(['lp_categories']); - - return ( - - - -
- - - {t('title')} - -
-
- {t('create.title')} -
-
- -
-
- ); -} -
- - -# 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, - - - -'use client'; - -import * as React from 'react'; - -export default function Page(): React.JSX.Element { - return <>helloworld; -} - - - -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { LpCategoryEditForm } from '@/components/dashboard/lp/categories/lp-category-edit-form'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(['lp_categories']); - - React.useEffect(() => { - // console.log('helloworld'); - }, []); - - return ( - - - -
- - - {t('edit.title')} - -
-
- {t('edit.title')} -
-
- -
-
- ); -} -
- - -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[]; - - - -'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 { COL_QUIZ_LP_CATEGORIES } from '@/constants'; -import { LoadingButton } from '@mui/lab'; -import Box from '@mui/material/Box'; -import Card from '@mui/material/Card'; -import Divider from '@mui/material/Divider'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; -import type { ListResult, RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import isDevelopment from '@/lib/check-is-development'; -import { logger } from '@/lib/default-logger'; -import { pb } from '@/lib/pb'; -import { toast } from '@/components/core/toaster'; -import ErrorDisplay from '@/components/dashboard/error'; -import { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants'; -import { LpCategoriesFilters } from '@/components/dashboard/lp/categories/lp-categories-filters'; -import type { Filters } from '@/components/dashboard/lp/categories/lp-categories-filters'; -import { LpCategoriesPagination } from '@/components/dashboard/lp/categories/lp-categories-pagination'; -import { LpCategoriesSelectionProvider } from '@/components/dashboard/lp/categories/lp-categories-selection-context'; -import { LpCategoriesTable } from '@/components/dashboard/lp/categories/lp-categories-table'; -import type { LpCategory } from '@/components/dashboard/lp/categories/type'; -import FormLoading from '@/components/loading'; - -export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { t } = useTranslation(['lp_categories']); - const { email, phone, sortDir, status, name, visible, type } = searchParams; - const router = useRouter(); - const [lessonCategoriesData, setLessonCategoriesData] = React.useState([]); - // - - const [isLoadingAddPage, setIsLoadingAddPage] = React.useState(false); - const [showLoading, setShowLoading] = React.useState(true); - const [showError, setShowError] = React.useState({ show: false, detail: '' }); - // - const [rowsPerPage, setRowsPerPage] = React.useState(5); - const [f, setF] = React.useState([]); - const [currentPage, setCurrentPage] = React.useState(1); - const [recordCount, setRecordCount] = React.useState(0); - const [listOption, setListOption] = React.useState({}); - const [listSort, setListSort] = React.useState({}); - - // - const sortedLessonCategories = applySort(lessonCategoriesData, sortDir); - const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status }); - - const reloadRows = async (): Promise => { - try { - const models: ListResult = await pb - .collection(COL_QUIZ_LP_CATEGORIES) - .getList(currentPage + 1, rowsPerPage, listOption); - const { items, totalItems } = models; - const tempLessonTypes: LpCategory[] = items.map((lt) => { - return { ...defaultLpCategory, ...lt }; - }); - - setLessonCategoriesData(tempLessonTypes); - setRecordCount(totalItems); - setF(tempLessonTypes); - // console.log({ currentPage, f }); - } catch (error) { - // - logger.error(error); - setShowError({ - // - show: true, - detail: JSON.stringify(error, null, 2), - }); - } finally { - setShowLoading(false); - } - }; - - const [lastListOption, setLastListOption] = React.useState({}); - const isFirstRun = React.useRef(false); - React.useEffect(() => { - if (!isFirstRun.current) { - isFirstRun.current = true; - } else { - if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { - // reset page number as tab changes - setLastListOption(listOption); - setCurrentPage(0); - void reloadRows(); - } else { - void reloadRows(); - } - } - }, [currentPage, rowsPerPage, listOption]); - - React.useEffect(() => { - let tempFilter = [], - tempSortDir = ''; - - if (visible) { - tempFilter.push(`visible = "${visible}"`); - } - - if (sortDir) { - tempSortDir = `-created`; - } - - if (name) { - tempFilter.push(`name ~ "%${name}%"`); - } - - if (type) { - tempFilter.push(`type ~ "%${type}%"`); - } - - let preFinalListOption = {}; - if (tempFilter.length > 0) { - preFinalListOption = { filter: tempFilter.join(' && ') }; - } - if (tempSortDir.length > 0) { - preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; - } - setListOption(preFinalListOption); - // setListOption({ - // filter: tempFilter.join(' && '), - // sort: tempSortDir, - // // - // }); - }, [visible, sortDir, name, type]); - - // return <>helloworld; - - if (showLoading) return ; - - if (showError.show) - return ( - - ); - - return ( - - - - - {t('list.title')} - - - { - setIsLoadingAddPage(true); - router.push(paths.dashboard.lp_categories.create); - }} - startIcon={} - variant="contained" - > - {t('list.add')} - - - - - - - - - - - - - - - - -
{JSON.stringify(f, null, 2)}
-
-
- ); -} - -// Sorting and filtering has to be done on the server. - -function applySort(row: LpCategory[], sortDir: 'asc' | 'desc' | undefined): LpCategory[] { - return row.sort((a, b) => { - if (sortDir === 'asc') { - return a.createdAt.getTime() - b.createdAt.getTime(); - } - - return b.createdAt.getTime() - a.createdAt.getTime(); - }); -} - -function applyFilters(row: LpCategory[], { email, phone, status, name, visible }: Filters): LpCategory[] { - return row.filter((item) => { - if (email) { - if (!item.email?.toLowerCase().includes(email.toLowerCase())) { - return false; - } - } - - if (phone) { - if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { - return false; - } - } - - if (status) { - if (item.status !== status) { - return false; - } - } - - if (name) { - if (!item.name?.toLowerCase().includes(name.toLowerCase())) { - return false; - } - } - - if (visible) { - if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) { - return false; - } - } - - return true; - }); -} - -interface PageProps { - searchParams: { - email?: string; - phone?: string; - sortDir?: 'asc' | 'desc'; - status?: string; - name?: string; - visible?: string; - type?: string; - // - }; -} -
- - -'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 => ( - - ) - )} - - - ); -} - - - -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; -import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; -import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; -import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; -import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; -import { Grid } from '@mui/material'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import type { RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { pb } from '@/lib/pb'; -import { toast } from '@/components/core/toaster'; -import ErrorDisplay from '@/components/dashboard/error'; -import { defaultLpQuestion } from '@/components/dashboard/lp/questions/_constants.ts'; -import { Notifications } from '@/components/dashboard/lp/questions/notifications'; -import type { LpQuestion } from '@/components/dashboard/lp/questions/type'; -import FormLoading from '@/components/loading'; - -import BasicDetailCard from './BasicDetailCard'; -import TitleCard from './TitleCard'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(); - const router = useRouter(); - // - const { cat_id: catId } = useParams<{ cat_id: string }>(); - // - const [showLoading, setShowLoading] = React.useState(true); - const [showError, setShowError] = React.useState({ show: false, detail: '' }); - - // - const [showLessonQuestion, setShowLessonQuestion] = React.useState(defaultLpQuestion); - - function handleEditClick() { - router.push(paths.dashboard.lp_questions.edit(showLessonQuestion.id)); - } - - React.useEffect(() => { - if (catId) { - pb.collection(COL_QUIZ_LP_QUESTIONS) - .getOne(catId) - .then((model: RecordModel) => { - setShowLessonQuestion({ ...defaultLpQuestion, ...model }); - }) - .catch((err) => { - logger.error(err); - toast(t('list.error')); - - setShowError({ show: true, detail: JSON.stringify(err) }); - }) - .finally(() => { - setShowLoading(false); - }); - } - }, [catId]); - - if (showLoading) return ; - if (showError.show) - return ( - - ); - - return ( - - - -
- - - {t('edit.title')} - -
- - - -
- - - - - - - - - - - - - - - -
-
- ); -} -
- - -'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: LpCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} - -export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element { - const { t } = useTranslation(); - - return ( - <> - - - {t('empty')} - -
- - {lpModel.cat_name} - - } - label={lpModel.visible} - size="small" - variant="outlined" - /> - - - {lpModel.slug} - -
-
-
- -
- - ); -} -
- - -'use client'; - -// RULES: -// T.B.A. -// -import * as React from 'react'; -import RouterLink from 'next/link'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { LpQuestionCreateForm } from '@/components/dashboard/lp/questions/lp-question-create-form'; - -export default function Page(): React.JSX.Element { - // RULES: follow the name of page directory - const { t } = useTranslation(['lp_questions']); - - return ( - - - -
- - - {t('title')} - -
-
- {t('create.title')} -
-
- -
-
- ); -} -
- - -# 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, - - - -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { LpQuestionEditForm } from '@/components/dashboard/lp/questions/lp-question-edit-form'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(['lp_questions']); - - React.useEffect(() => { - // console.log('helloworld'); - }, []); - - return ( - - - -
- - - {t('edit.title')} - -
-
- {t('edit.title')} -
-
- -
-
- ); -} -
- - -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[]; - - - -'use client'; - -// RULES: -// contains list page for lp_questions (QuizLPQuestions) -// contain definition to collection only -// -import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; -import { LoadingButton } from '@mui/lab'; -import Box from '@mui/material/Box'; -import Card from '@mui/material/Card'; -import Divider from '@mui/material/Divider'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; -import type { ListResult, RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import isDevelopment from '@/lib/check-is-development'; -import { logger } from '@/lib/default-logger'; -import { pb } from '@/lib/pb'; -import { toast } from '@/components/core/toaster'; -import ErrorDisplay from '@/components/dashboard/error'; -import { defaultLpQuestion } from '@/components/dashboard/lp/questions/_constants'; -import { LpQuestionsFilters } from '@/components/dashboard/lp/questions/lp-questions-filters'; -import type { Filters } from '@/components/dashboard/lp/questions/lp-questions-filters'; -import { LpQuestionsPagination } from '@/components/dashboard/lp/questions/lp-questions-pagination'; -import { LpQuestionsSelectionProvider } from '@/components/dashboard/lp/questions/lp-questions-selection-context'; -import { LpQuestionsTable } from '@/components/dashboard/lp/questions/lp-questions-table'; -import type { LpQuestion } from '@/components/dashboard/lp/questions/type'; -import FormLoading from '@/components/loading'; - -export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { t } = useTranslation(['lp_questions']); - const { email, phone, sortDir, status, name, visible, type } = searchParams; - const router = useRouter(); - const [lessonQuestionsData, setLessonCategoriesData] = React.useState([]); - // - - const [isLoadingAddPage, setIsLoadingAddPage] = React.useState(false); - const [showLoading, setShowLoading] = React.useState(true); - const [showError, setShowError] = React.useState({ show: false, detail: '' }); - // - const [rowsPerPage, setRowsPerPage] = React.useState(5); - const [f, setF] = React.useState([]); - const [currentPage, setCurrentPage] = React.useState(0); - const [recordCount, setRecordCount] = React.useState(0); - const [listOption, setListOption] = React.useState({}); - const [listSort, setListSort] = React.useState({}); - - // - const sortedLessonCategories = applySort(lessonQuestionsData, sortDir); - const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status }); - - const reloadRows = async (): Promise => { - try { - const models: ListResult = await pb - .collection(COL_QUIZ_LP_QUESTIONS) - .getList(currentPage + 1, rowsPerPage, listOption); - const { items, totalItems } = models; - const tempLessonTypes: LpQuestion[] = items.map((lt) => { - return { ...defaultLpQuestion, ...lt }; - }); - - setLessonCategoriesData(tempLessonTypes); - setRecordCount(totalItems); - setF(tempLessonTypes); - // console.log({ currentPage, f }); - } catch (error) { - // - logger.error(error); - setShowError({ - // - show: true, - detail: JSON.stringify(error, null, 2), - }); - } finally { - setShowLoading(false); - } - }; - - const [lastListOption, setLastListOption] = React.useState({}); - const isFirstRun = React.useRef(false); - React.useEffect(() => { - if (!isFirstRun.current) { - isFirstRun.current = true; - } else { - if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { - // reset page number as tab changes - setLastListOption(listOption); - setCurrentPage(0); - void reloadRows(); - } else { - void reloadRows(); - } - } - }, [currentPage, rowsPerPage, listOption]); - - React.useEffect(() => { - let tempFilter = [], - tempSortDir = ''; - - if (visible) { - tempFilter.push(`visible = "${visible}"`); - } - - if (sortDir) { - tempSortDir = `-created`; - } - - if (name) { - tempFilter.push(`name ~ "%${name}%"`); - } - - if (type) { - tempFilter.push(`type ~ "%${type}%"`); - } - - let preFinalListOption = {}; - if (tempFilter.length > 0) { - preFinalListOption = { filter: tempFilter.join(' && ') }; - } - if (tempSortDir.length > 0) { - preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; - } - setListOption(preFinalListOption); - // setListOption({ - // filter: tempFilter.join(' && '), - // sort: tempSortDir, - // // - // }); - }, [visible, sortDir, name, type]); - - // return <>helloworld; - - if (showLoading) return ; - - if (showError.show) - return ( - - ); - - return ( - - - - - {t('list.title')} - - - { - setIsLoadingAddPage(true); - router.push(paths.dashboard.lp_questions.create); - }} - startIcon={} - variant="contained" - > - {t('list.add')} - - - - - - - - - - - - - - - - -
{JSON.stringify(f, null, 2)}
-
-
- ); -} - -// Sorting and filtering has to be done on the server. - -function applySort(row: LpQuestion[], sortDir: 'asc' | 'desc' | undefined): LpQuestion[] { - return row.sort((a, b) => { - if (sortDir === 'asc') { - return a.createdAt.getTime() - b.createdAt.getTime(); - } - - return b.createdAt.getTime() - a.createdAt.getTime(); - }); -} - -function applyFilters(row: LpQuestion[], { email, phone, status, name, visible }: Filters): LpQuestion[] { - return row.filter((item) => { - if (email) { - if (!item.email?.toLowerCase().includes(email.toLowerCase())) { - return false; - } - } - - if (phone) { - if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { - return false; - } - } - - if (status) { - if (item.status !== status) { - return false; - } - } - - if (name) { - if (!item.name?.toLowerCase().includes(name.toLowerCase())) { - return false; - } - } - - if (visible) { - if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) { - return false; - } - } - - return true; - }); -} - -interface PageProps { - searchParams: { - email?: string; - phone?: string; - sortDir?: 'asc' | 'desc'; - status?: string; - name?: string; - visible?: string; - type?: string; - // - }; -} -
- -
diff --git a/002_source/cms/src/app/dashboard/cr_categories/[customerId]/page.tsx b/002_source/cms/src/app/dashboard/cr_categories/[customerId]/page.tsx deleted file mode 100644 index edf064e..0000000 --- a/002_source/cms/src/app/dashboard/cr_categories/[customerId]/page.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import * as React from 'react'; -import type { Metadata } from 'next'; -import RouterLink from 'next/link'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import IconButton from '@mui/material/IconButton'; -import LinearProgress from '@mui/material/LinearProgress'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import Grid from '@mui/material/Unstable_Grid2'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; -import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard'; -import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House'; -import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; -import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; -import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning'; -import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; - -import { config } from '@/config'; -import { paths } from '@/paths'; -import { dayjs } from '@/lib/dayjs'; -import { PropertyItem } from '@/components/core/property-item'; -import { PropertyList } from '@/components/core/property-list'; -import { Notifications } from '@/components/dashboard/customer/notifications'; -import { Payments } from '@/components/dashboard/customer/payments'; -import type { Address } from '@/components/dashboard/customer/shipping-address'; -import { ShippingAddress } from '@/components/dashboard/customer/shipping-address'; - -export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; - -export default function Page(): React.JSX.Element { - return ( - - - -
- - - Customers - -
- - - - MV - -
- - Miron Vitold - } - label="Active" - size="small" - variant="outlined" - /> - - - miron.vitold@domain.com - -
-
-
- -
-
-
- - - - - - - - } - avatar={ - - - - } - title="Basic details" - /> - } - orientation="vertical" - sx={{ '--PropertyItem-padding': '12px 24px' }} - > - {( - [ - { key: 'Customer ID', value: }, - { key: 'Name', value: 'Miron Vitold' }, - { key: 'Email', value: 'miron.vitold@domain.com' }, - { key: 'Phone', value: '(425) 434-5535' }, - { key: 'Company', value: 'Devias IO' }, - { - key: 'Quota', - value: ( - - - - 50% - - - ), - }, - ] satisfies { key: string; value: React.ReactNode }[] - ).map( - (item): React.JSX.Element => ( - - ) - )} - - - - - - - } - title="Security" - /> - - -
- -
- - A deleted customer cannot be restored. All data will be permanently removed. - -
-
-
-
-
- - - - - }> - Edit - - } - avatar={ - - - - } - title="Billing details" - /> - - - } sx={{ '--PropertyItem-padding': '16px' }}> - {( - [ - { key: 'Credit card', value: '**** 4142' }, - { key: 'Country', value: 'United States' }, - { key: 'State', value: 'Michigan' }, - { key: 'City', value: 'Southfield' }, - { key: 'Address', value: '1721 Bartlett Avenue, 48034' }, - { key: 'Tax ID', value: 'EU87956621' }, - ] satisfies { key: string; value: React.ReactNode }[] - ).map( - (item): React.JSX.Element => ( - - ) - )} - - - - - - }> - Add - - } - avatar={ - - - - } - title="Shipping addresses" - /> - - - {( - [ - { - id: 'ADR-001', - country: 'United States', - state: 'Michigan', - city: 'Lansing', - zipCode: '48933', - street: '480 Haven Lane', - primary: true, - }, - { - id: 'ADR-002', - country: 'United States', - state: 'Missouri', - city: 'Springfield', - zipCode: '65804', - street: '4807 Lighthouse Drive', - }, - ] satisfies Address[] - ).map((address) => ( - - - - ))} - - - - - - -
-
-
- ); -} diff --git a/002_source/cms/src/app/dashboard/cr_categories/create/page.tsx b/002_source/cms/src/app/dashboard/cr_categories/create/page.tsx deleted file mode 100644 index a0460ab..0000000 --- a/002_source/cms/src/app/dashboard/cr_categories/create/page.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from 'react'; -import type { Metadata } from 'next'; -import RouterLink from 'next/link'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; - -import { config } from '@/config'; -import { paths } from '@/paths'; -import { CustomerCreateForm } from '@/components/dashboard/customer/customer-create-form'; - -export const metadata = { title: `Create | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; - -export default function Page(): React.JSX.Element { - return ( - - - -
- - - Customers - -
-
- Create customer -
-
- -
-
- ); -} diff --git a/002_source/cms/src/app/dashboard/cr_categories/page.tsx b/002_source/cms/src/app/dashboard/cr_categories/page.tsx deleted file mode 100644 index 552f8dd..0000000 --- a/002_source/cms/src/app/dashboard/cr_categories/page.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import * as React from 'react'; -import type { Metadata } from 'next'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import Divider from '@mui/material/Divider'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; - -import { config } from '@/config'; -import { dayjs } from '@/lib/dayjs'; -import { CustomersFilters } from '@/components/dashboard/customer/customers-filters'; -import type { Filters } from '@/components/dashboard/customer/customers-filters'; -import { CustomersPagination } from '@/components/dashboard/customer/customers-pagination'; -import { CustomersSelectionProvider } from '@/components/dashboard/customer/customers-selection-context'; -import { CustomersTable } from '@/components/dashboard/customer/customers-table'; -import type { Customer } from '@/components/dashboard/customer/customers-table'; - -export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; - -const customers = [ - { - 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[]; - -interface PageProps { - searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string }; -} - -export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { email, phone, sortDir, status } = searchParams; - - const sortedCustomers = applySort(customers, sortDir); - const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status }); - - return ( - - - - - Customers - - - - - - - - - - - - - - - - - - - ); -} - -// Sorting and filtering has to be done on the server. - -function applySort(row: Customer[], sortDir: 'asc' | 'desc' | undefined): Customer[] { - return row.sort((a, b) => { - if (sortDir === 'asc') { - return a.createdAt.getTime() - b.createdAt.getTime(); - } - - return b.createdAt.getTime() - a.createdAt.getTime(); - }); -} - -function applyFilters(row: Customer[], { email, phone, status }: Filters): Customer[] { - return row.filter((item) => { - if (email) { - if (!item.email?.toLowerCase().includes(email.toLowerCase())) { - return false; - } - } - - if (phone) { - if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { - return false; - } - } - - if (status) { - if (item.status !== status) { - return false; - } - } - - return true; - }); -} diff --git a/002_source/cms/src/app/dashboard/cr_questions/[customerId]/page.tsx b/002_source/cms/src/app/dashboard/cr_questions/[customerId]/page.tsx deleted file mode 100644 index edf064e..0000000 --- a/002_source/cms/src/app/dashboard/cr_questions/[customerId]/page.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import * as React from 'react'; -import type { Metadata } from 'next'; -import RouterLink from 'next/link'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import IconButton from '@mui/material/IconButton'; -import LinearProgress from '@mui/material/LinearProgress'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import Grid from '@mui/material/Unstable_Grid2'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; -import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard'; -import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House'; -import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; -import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; -import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning'; -import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; - -import { config } from '@/config'; -import { paths } from '@/paths'; -import { dayjs } from '@/lib/dayjs'; -import { PropertyItem } from '@/components/core/property-item'; -import { PropertyList } from '@/components/core/property-list'; -import { Notifications } from '@/components/dashboard/customer/notifications'; -import { Payments } from '@/components/dashboard/customer/payments'; -import type { Address } from '@/components/dashboard/customer/shipping-address'; -import { ShippingAddress } from '@/components/dashboard/customer/shipping-address'; - -export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; - -export default function Page(): React.JSX.Element { - return ( - - - -
- - - Customers - -
- - - - MV - -
- - Miron Vitold - } - label="Active" - size="small" - variant="outlined" - /> - - - miron.vitold@domain.com - -
-
-
- -
-
-
- - - - - - - - } - avatar={ - - - - } - title="Basic details" - /> - } - orientation="vertical" - sx={{ '--PropertyItem-padding': '12px 24px' }} - > - {( - [ - { key: 'Customer ID', value: }, - { key: 'Name', value: 'Miron Vitold' }, - { key: 'Email', value: 'miron.vitold@domain.com' }, - { key: 'Phone', value: '(425) 434-5535' }, - { key: 'Company', value: 'Devias IO' }, - { - key: 'Quota', - value: ( - - - - 50% - - - ), - }, - ] satisfies { key: string; value: React.ReactNode }[] - ).map( - (item): React.JSX.Element => ( - - ) - )} - - - - - - - } - title="Security" - /> - - -
- -
- - A deleted customer cannot be restored. All data will be permanently removed. - -
-
-
-
-
- - - - - }> - Edit - - } - avatar={ - - - - } - title="Billing details" - /> - - - } sx={{ '--PropertyItem-padding': '16px' }}> - {( - [ - { key: 'Credit card', value: '**** 4142' }, - { key: 'Country', value: 'United States' }, - { key: 'State', value: 'Michigan' }, - { key: 'City', value: 'Southfield' }, - { key: 'Address', value: '1721 Bartlett Avenue, 48034' }, - { key: 'Tax ID', value: 'EU87956621' }, - ] satisfies { key: string; value: React.ReactNode }[] - ).map( - (item): React.JSX.Element => ( - - ) - )} - - - - - - }> - Add - - } - avatar={ - - - - } - title="Shipping addresses" - /> - - - {( - [ - { - id: 'ADR-001', - country: 'United States', - state: 'Michigan', - city: 'Lansing', - zipCode: '48933', - street: '480 Haven Lane', - primary: true, - }, - { - id: 'ADR-002', - country: 'United States', - state: 'Missouri', - city: 'Springfield', - zipCode: '65804', - street: '4807 Lighthouse Drive', - }, - ] satisfies Address[] - ).map((address) => ( - - - - ))} - - - - - - -
-
-
- ); -} diff --git a/002_source/cms/src/app/dashboard/cr_questions/create/page.tsx b/002_source/cms/src/app/dashboard/cr_questions/create/page.tsx deleted file mode 100644 index a0460ab..0000000 --- a/002_source/cms/src/app/dashboard/cr_questions/create/page.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from 'react'; -import type { Metadata } from 'next'; -import RouterLink from 'next/link'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; - -import { config } from '@/config'; -import { paths } from '@/paths'; -import { CustomerCreateForm } from '@/components/dashboard/customer/customer-create-form'; - -export const metadata = { title: `Create | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; - -export default function Page(): React.JSX.Element { - return ( - - - -
- - - Customers - -
-
- Create customer -
-
- -
-
- ); -} diff --git a/002_source/cms/src/app/dashboard/cr_questions/page.tsx b/002_source/cms/src/app/dashboard/cr_questions/page.tsx deleted file mode 100644 index 552f8dd..0000000 --- a/002_source/cms/src/app/dashboard/cr_questions/page.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import * as React from 'react'; -import type { Metadata } from 'next'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import Divider from '@mui/material/Divider'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; - -import { config } from '@/config'; -import { dayjs } from '@/lib/dayjs'; -import { CustomersFilters } from '@/components/dashboard/customer/customers-filters'; -import type { Filters } from '@/components/dashboard/customer/customers-filters'; -import { CustomersPagination } from '@/components/dashboard/customer/customers-pagination'; -import { CustomersSelectionProvider } from '@/components/dashboard/customer/customers-selection-context'; -import { CustomersTable } from '@/components/dashboard/customer/customers-table'; -import type { Customer } from '@/components/dashboard/customer/customers-table'; - -export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; - -const customers = [ - { - 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[]; - -interface PageProps { - searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string }; -} - -export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { email, phone, sortDir, status } = searchParams; - - const sortedCustomers = applySort(customers, sortDir); - const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status }); - - return ( - - - - - Customers - - - - - - - - - - - - - - - - - - - ); -} - -// Sorting and filtering has to be done on the server. - -function applySort(row: Customer[], sortDir: 'asc' | 'desc' | undefined): Customer[] { - return row.sort((a, b) => { - if (sortDir === 'asc') { - return a.createdAt.getTime() - b.createdAt.getTime(); - } - - return b.createdAt.getTime() - a.createdAt.getTime(); - }); -} - -function applyFilters(row: Customer[], { email, phone, status }: Filters): Customer[] { - return row.filter((item) => { - if (email) { - if (!item.email?.toLowerCase().includes(email.toLowerCase())) { - return false; - } - } - - if (phone) { - if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { - return false; - } - } - - if (status) { - if (item.status !== status) { - return false; - } - } - - return true; - }); -} diff --git a/002_source/cms/src/app/dashboard/lp/repomix-output.xml b/002_source/cms/src/app/dashboard/lp/repomix-output.xml deleted file mode 100644 index c11e03d..0000000 --- a/002_source/cms/src/app/dashboard/lp/repomix-output.xml +++ /dev/null @@ -1,1783 +0,0 @@ -This file is a merged representation of the entire codebase, combined into a single document by Repomix. - - -This section contains a summary of this file. - - -This file contains a packed representation of the entire repository's contents. -It is designed to be easily consumable by AI systems for analysis, code review, -or other automated processes. - - - -The content is organized as follows: -1. This summary section -2. Repository information -3. Directory structure -4. Repository files, each consisting of: - - File path as an attribute - - Full contents of the file - - - -- This file should be treated as read-only. Any changes should be made to the - original repository files, not this packed version. -- When processing this file, use the file path to distinguish - between different files in the repository. -- Be aware that this file may contain sensitive information. Handle it with - the same level of security as you would the original repository. - - - -- Some files may have been excluded based on .gitignore rules and Repomix's configuration -- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files -- Files matching patterns in .gitignore are excluded -- Files matching default ignore patterns are excluded -- Files are sorted by Git change count (files with more changes are at the bottom) - - - - - - - - - -categories/ - [cat_id]/ - BasicDetailCard.tsx - page.tsx - TitleCard.tsx - create/ - page copy 2.tsx - page copy.tsx - page.tsx - edit/ - [cat_id]/ - _PROMPT.md - page copy.tsx - page.tsx - lp-categories-sample-data.tsx - page.tsx -questions/ - [cat_id]/ - BasicDetailCard.tsx - page.tsx - TitleCard.tsx - create/ - page.tsx - edit/ - [cat_id]/ - _PROMPT.md - page.tsx - lp-categories-sample-data.tsx - page.tsx - - - -This section contains the contents of the repository's files. - - -'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 => ( - - ) - )} - - - ); -} - - - -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; -import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; -import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; -import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; -import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Grid from '@mui/material/Unstable_Grid2'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import type { RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { pb } from '@/lib/pb'; -import { toast } from '@/components/core/toaster'; -import ErrorDisplay from '@/components/dashboard/error'; -import { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants.ts'; -import { Notifications } from '@/components/dashboard/lp/categories/notifications'; -import type { LpCategory } from '@/components/dashboard/lp/categories/type'; -import FormLoading from '@/components/loading'; - -import BasicDetailCard from './BasicDetailCard'; -import TitleCard from './TitleCard'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(); - const router = useRouter(); - // - const { cat_id: catId } = useParams<{ cat_id: string }>(); - // - const [showLoading, setShowLoading] = React.useState(true); - const [showError, setShowError] = React.useState({ show: false, detail: '' }); - - // - const [showLessonCategory, setShowLessonCategory] = React.useState(defaultLpCategory); - - function handleEditClick() { - router.push(paths.dashboard.lp_categories.edit(showLessonCategory.id)); - } - - React.useEffect(() => { - if (catId) { - pb.collection(COL_QUIZ_LP_CATEGORIES) - .getOne(catId) - .then((model: RecordModel) => { - setShowLessonCategory({ ...defaultLpCategory, ...model }); - }) - .catch((err) => { - logger.error(err); - toast(t('list.error')); - - setShowError({ show: true, detail: JSON.stringify(err) }); - }) - .finally(() => { - setShowLoading(false); - }); - } - }, [catId]); - - if (showLoading) return ; - if (showError.show) - return ( - - ); - - return ( - - - -
- - - {t('list.title')} - -
- - - -
- - - - - - - - - - - - - - - -
-
- ); -} -
- - -'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: LpCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} - -export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element { - const { t } = useTranslation(); - - return ( - <> - - - {t('empty')} - -
- - {lpModel.cat_name} - - } - label={lpModel.visible} - size="small" - variant="outlined" - /> - - - {lpModel.slug} - -
-
-
- -
- - ); -} -
- - -'use client'; - -// RULES: -// T.B.A. -// -import * as React from 'react'; -import RouterLink from 'next/link'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { LpCategoryCreateForm } from '@/components/dashboard/lp/categories/lp-category-create-form'; - -export default function Page(): React.JSX.Element { - // RULES: follow the name of page directory - const { t } = useTranslation(['lp_categories']); - - return ( - - - -
- - - {t('title')} - -
-
- {t('create.title')} -
-
- -
-
- ); -} -
- - -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { LessonCategoryCreateForm } from '@/components/dashboard/lesson_category/lesson-category-create-form'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(); - return ( - - - -
- - - {t('title', { ns: 'lesson_category' })} - -
-
- {t('create.title', { ns: 'lesson_category' })} -
-
- -
-
- ); -} -
- - -'use client'; - -// RULES: -// T.B.A. -// -import * as React from 'react'; -import RouterLink from 'next/link'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { LpCategoryCreateForm } from '@/components/dashboard/lp/categories/lp-category-create-form'; - -export default function Page(): React.JSX.Element { - // RULES: follow the name of page directory - const { t } = useTranslation(['lp_categories']); - - return ( - - - -
- - - {t('title')} - -
-
- {t('create.title')} -
-
- -
-
- ); -} -
- - -# 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, - - - -'use client'; - -import * as React from 'react'; - -export default function Page(): React.JSX.Element { - return <>helloworld; -} - - - -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { LpCategoryEditForm } from '@/components/dashboard/lp/categories/lp-category-edit-form'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(['lp_categories']); - - React.useEffect(() => { - // console.log('helloworld'); - }, []); - - return ( - - - -
- - - {t('edit.title')} - -
-
- {t('edit.title')} -
-
- -
-
- ); -} -
- - -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[]; - - - -'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 { COL_QUIZ_LP_CATEGORIES } from '@/constants'; -import { LoadingButton } from '@mui/lab'; -import Box from '@mui/material/Box'; -import Card from '@mui/material/Card'; -import Divider from '@mui/material/Divider'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; -import type { ListResult, RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import isDevelopment from '@/lib/check-is-development'; -import { logger } from '@/lib/default-logger'; -import { pb } from '@/lib/pb'; -import { toast } from '@/components/core/toaster'; -import ErrorDisplay from '@/components/dashboard/error'; -import { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants'; -import { LpCategoriesFilters } from '@/components/dashboard/lp/categories/lp-categories-filters'; -import type { Filters } from '@/components/dashboard/lp/categories/lp-categories-filters'; -import { LpCategoriesPagination } from '@/components/dashboard/lp/categories/lp-categories-pagination'; -import { LpCategoriesSelectionProvider } from '@/components/dashboard/lp/categories/lp-categories-selection-context'; -import { LpCategoriesTable } from '@/components/dashboard/lp/categories/lp-categories-table'; -import type { LpCategory } from '@/components/dashboard/lp/categories/type'; -import FormLoading from '@/components/loading'; - -export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { t } = useTranslation(['lp_categories']); - const { email, phone, sortDir, status, name, visible, type } = searchParams; - const router = useRouter(); - const [lessonCategoriesData, setLessonCategoriesData] = React.useState([]); - // - - const [isLoadingAddPage, setIsLoadingAddPage] = React.useState(false); - const [showLoading, setShowLoading] = React.useState(true); - const [showError, setShowError] = React.useState({ show: false, detail: '' }); - // - const [rowsPerPage, setRowsPerPage] = React.useState(5); - const [f, setF] = React.useState([]); - const [currentPage, setCurrentPage] = React.useState(1); - const [recordCount, setRecordCount] = React.useState(0); - const [listOption, setListOption] = React.useState({}); - const [listSort, setListSort] = React.useState({}); - - // - const sortedLessonCategories = applySort(lessonCategoriesData, sortDir); - const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status }); - - const reloadRows = async (): Promise => { - try { - const models: ListResult = await pb - .collection(COL_QUIZ_LP_CATEGORIES) - .getList(currentPage + 1, rowsPerPage, listOption); - const { items, totalItems } = models; - const tempLessonTypes: LpCategory[] = items.map((lt) => { - return { ...defaultLpCategory, ...lt }; - }); - - setLessonCategoriesData(tempLessonTypes); - setRecordCount(totalItems); - setF(tempLessonTypes); - // console.log({ currentPage, f }); - } catch (error) { - // - logger.error(error); - setShowError({ - // - show: true, - detail: JSON.stringify(error, null, 2), - }); - } finally { - setShowLoading(false); - } - }; - - const [lastListOption, setLastListOption] = React.useState({}); - const isFirstRun = React.useRef(false); - React.useEffect(() => { - if (!isFirstRun.current) { - isFirstRun.current = true; - } else { - if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { - // reset page number as tab changes - setLastListOption(listOption); - setCurrentPage(0); - void reloadRows(); - } else { - void reloadRows(); - } - } - }, [currentPage, rowsPerPage, listOption]); - - React.useEffect(() => { - let tempFilter = [], - tempSortDir = ''; - - if (visible) { - tempFilter.push(`visible = "${visible}"`); - } - - if (sortDir) { - tempSortDir = `-created`; - } - - if (name) { - tempFilter.push(`name ~ "%${name}%"`); - } - - if (type) { - tempFilter.push(`type ~ "%${type}%"`); - } - - let preFinalListOption = {}; - if (tempFilter.length > 0) { - preFinalListOption = { filter: tempFilter.join(' && ') }; - } - if (tempSortDir.length > 0) { - preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; - } - setListOption(preFinalListOption); - // setListOption({ - // filter: tempFilter.join(' && '), - // sort: tempSortDir, - // // - // }); - }, [visible, sortDir, name, type]); - - // return <>helloworld; - - if (showLoading) return ; - - if (showError.show) - return ( - - ); - - return ( - - - - - {t('list.title')} - - - { - setIsLoadingAddPage(true); - router.push(paths.dashboard.lp_categories.create); - }} - startIcon={} - variant="contained" - > - {t('list.add')} - - - - - - - - - - - - - - - - -
{JSON.stringify(f, null, 2)}
-
-
- ); -} - -// Sorting and filtering has to be done on the server. - -function applySort(row: LpCategory[], sortDir: 'asc' | 'desc' | undefined): LpCategory[] { - return row.sort((a, b) => { - if (sortDir === 'asc') { - return a.createdAt.getTime() - b.createdAt.getTime(); - } - - return b.createdAt.getTime() - a.createdAt.getTime(); - }); -} - -function applyFilters(row: LpCategory[], { email, phone, status, name, visible }: Filters): LpCategory[] { - return row.filter((item) => { - if (email) { - if (!item.email?.toLowerCase().includes(email.toLowerCase())) { - return false; - } - } - - if (phone) { - if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { - return false; - } - } - - if (status) { - if (item.status !== status) { - return false; - } - } - - if (name) { - if (!item.name?.toLowerCase().includes(name.toLowerCase())) { - return false; - } - } - - if (visible) { - if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) { - return false; - } - } - - return true; - }); -} - -interface PageProps { - searchParams: { - email?: string; - phone?: string; - sortDir?: 'asc' | 'desc'; - status?: string; - name?: string; - visible?: string; - type?: string; - // - }; -} -
- - -'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 => ( - - ) - )} - - - ); -} - - - -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; -import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; -import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; -import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; -import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; -import { Grid } from '@mui/material'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import type { RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { pb } from '@/lib/pb'; -import { toast } from '@/components/core/toaster'; -import ErrorDisplay from '@/components/dashboard/error'; -import { defaultLpQuestion } from '@/components/dashboard/lp/questions/_constants.ts'; -import { Notifications } from '@/components/dashboard/lp/questions/notifications'; -import type { LpQuestion } from '@/components/dashboard/lp/questions/type'; -import FormLoading from '@/components/loading'; - -import BasicDetailCard from './BasicDetailCard'; -import TitleCard from './TitleCard'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(); - const router = useRouter(); - // - const { cat_id: catId } = useParams<{ cat_id: string }>(); - // - const [showLoading, setShowLoading] = React.useState(true); - const [showError, setShowError] = React.useState({ show: false, detail: '' }); - - // - const [showLessonQuestion, setShowLessonQuestion] = React.useState(defaultLpQuestion); - - function handleEditClick() { - router.push(paths.dashboard.lp_questions.edit(showLessonQuestion.id)); - } - - React.useEffect(() => { - if (catId) { - pb.collection(COL_QUIZ_LP_QUESTIONS) - .getOne(catId) - .then((model: RecordModel) => { - setShowLessonQuestion({ ...defaultLpQuestion, ...model }); - }) - .catch((err) => { - logger.error(err); - toast(t('list.error')); - - setShowError({ show: true, detail: JSON.stringify(err) }); - }) - .finally(() => { - setShowLoading(false); - }); - } - }, [catId]); - - if (showLoading) return ; - if (showError.show) - return ( - - ); - - return ( - - - -
- - - {t('edit.title')} - -
- - - -
- - - - - - - - - - - - - - - -
-
- ); -} -
- - -'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: LpCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} - -export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element { - const { t } = useTranslation(); - - return ( - <> - - - {t('empty')} - -
- - {lpModel.cat_name} - - } - label={lpModel.visible} - size="small" - variant="outlined" - /> - - - {lpModel.slug} - -
-
-
- -
- - ); -} -
- - -'use client'; - -// RULES: -// T.B.A. -// -import * as React from 'react'; -import RouterLink from 'next/link'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { LpQuestionCreateForm } from '@/components/dashboard/lp/questions/lp-question-create-form'; - -export default function Page(): React.JSX.Element { - // RULES: follow the name of page directory - const { t } = useTranslation(['lp_questions']); - - return ( - - - -
- - - {t('title')} - -
-
- {t('create.title')} -
-
- -
-
- ); -} -
- - -# 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, - - - -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { LpQuestionEditForm } from '@/components/dashboard/lp/questions/lp-question-edit-form'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(['lp_questions']); - - React.useEffect(() => { - // console.log('helloworld'); - }, []); - - return ( - - - -
- - - {t('edit.title')} - -
-
- {t('edit.title')} -
-
- -
-
- ); -} -
- - -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[]; - - - -'use client'; - -// RULES: -// contains list page for lp_questions (QuizLPQuestions) -// contain definition to collection only -// -import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; -import { LoadingButton } from '@mui/lab'; -import Box from '@mui/material/Box'; -import Card from '@mui/material/Card'; -import Divider from '@mui/material/Divider'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; -import type { ListResult, RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import isDevelopment from '@/lib/check-is-development'; -import { logger } from '@/lib/default-logger'; -import { pb } from '@/lib/pb'; -import { toast } from '@/components/core/toaster'; -import ErrorDisplay from '@/components/dashboard/error'; -import { defaultLpQuestion } from '@/components/dashboard/lp/questions/_constants'; -import { LpQuestionsFilters } from '@/components/dashboard/lp/questions/lp-questions-filters'; -import type { Filters } from '@/components/dashboard/lp/questions/lp-questions-filters'; -import { LpQuestionsPagination } from '@/components/dashboard/lp/questions/lp-questions-pagination'; -import { LpQuestionsSelectionProvider } from '@/components/dashboard/lp/questions/lp-questions-selection-context'; -import { LpQuestionsTable } from '@/components/dashboard/lp/questions/lp-questions-table'; -import type { LpQuestion } from '@/components/dashboard/lp/questions/type'; -import FormLoading from '@/components/loading'; - -export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { t } = useTranslation(['lp_questions']); - const { email, phone, sortDir, status, name, visible, type } = searchParams; - const router = useRouter(); - const [lessonQuestionsData, setLessonCategoriesData] = React.useState([]); - // - - const [isLoadingAddPage, setIsLoadingAddPage] = React.useState(false); - const [showLoading, setShowLoading] = React.useState(true); - const [showError, setShowError] = React.useState({ show: false, detail: '' }); - // - const [rowsPerPage, setRowsPerPage] = React.useState(5); - const [f, setF] = React.useState([]); - const [currentPage, setCurrentPage] = React.useState(0); - const [recordCount, setRecordCount] = React.useState(0); - const [listOption, setListOption] = React.useState({}); - const [listSort, setListSort] = React.useState({}); - - // - const sortedLessonCategories = applySort(lessonQuestionsData, sortDir); - const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status }); - - const reloadRows = async (): Promise => { - try { - const models: ListResult = await pb - .collection(COL_QUIZ_LP_QUESTIONS) - .getList(currentPage + 1, rowsPerPage, listOption); - const { items, totalItems } = models; - const tempLessonTypes: LpQuestion[] = items.map((lt) => { - return { ...defaultLpQuestion, ...lt }; - }); - - setLessonCategoriesData(tempLessonTypes); - setRecordCount(totalItems); - setF(tempLessonTypes); - // console.log({ currentPage, f }); - } catch (error) { - // - logger.error(error); - setShowError({ - // - show: true, - detail: JSON.stringify(error, null, 2), - }); - } finally { - setShowLoading(false); - } - }; - - const [lastListOption, setLastListOption] = React.useState({}); - const isFirstRun = React.useRef(false); - React.useEffect(() => { - if (!isFirstRun.current) { - isFirstRun.current = true; - } else { - if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { - // reset page number as tab changes - setLastListOption(listOption); - setCurrentPage(0); - void reloadRows(); - } else { - void reloadRows(); - } - } - }, [currentPage, rowsPerPage, listOption]); - - React.useEffect(() => { - let tempFilter = [], - tempSortDir = ''; - - if (visible) { - tempFilter.push(`visible = "${visible}"`); - } - - if (sortDir) { - tempSortDir = `-created`; - } - - if (name) { - tempFilter.push(`name ~ "%${name}%"`); - } - - if (type) { - tempFilter.push(`type ~ "%${type}%"`); - } - - let preFinalListOption = {}; - if (tempFilter.length > 0) { - preFinalListOption = { filter: tempFilter.join(' && ') }; - } - if (tempSortDir.length > 0) { - preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; - } - setListOption(preFinalListOption); - // setListOption({ - // filter: tempFilter.join(' && '), - // sort: tempSortDir, - // // - // }); - }, [visible, sortDir, name, type]); - - // return <>helloworld; - - if (showLoading) return ; - - if (showError.show) - return ( - - ); - - return ( - - - - - {t('list.title')} - - - { - setIsLoadingAddPage(true); - router.push(paths.dashboard.lp_questions.create); - }} - startIcon={} - variant="contained" - > - {t('list.add')} - - - - - - - - - - - - - - - - -
{JSON.stringify(f, null, 2)}
-
-
- ); -} - -// Sorting and filtering has to be done on the server. - -function applySort(row: LpQuestion[], sortDir: 'asc' | 'desc' | undefined): LpQuestion[] { - return row.sort((a, b) => { - if (sortDir === 'asc') { - return a.createdAt.getTime() - b.createdAt.getTime(); - } - - return b.createdAt.getTime() - a.createdAt.getTime(); - }); -} - -function applyFilters(row: LpQuestion[], { email, phone, status, name, visible }: Filters): LpQuestion[] { - return row.filter((item) => { - if (email) { - if (!item.email?.toLowerCase().includes(email.toLowerCase())) { - return false; - } - } - - if (phone) { - if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { - return false; - } - } - - if (status) { - if (item.status !== status) { - return false; - } - } - - if (name) { - if (!item.name?.toLowerCase().includes(name.toLowerCase())) { - return false; - } - } - - if (visible) { - if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) { - return false; - } - } - - return true; - }); -} - -interface PageProps { - searchParams: { - email?: string; - phone?: string; - sortDir?: 'asc' | 'desc'; - status?: string; - name?: string; - visible?: string; - type?: string; - // - }; -} -
- -
diff --git a/002_source/cms/src/components/dashboard/cr/categories/_PROMPT.MD b/002_source/cms/src/components/dashboard/cr/categories/_PROMPT.MD deleted file mode 100644 index 93ce5a6..0000000 --- a/002_source/cms/src/components/dashboard/cr/categories/_PROMPT.MD +++ /dev/null @@ -1 +0,0 @@ -please review and add translations, e.g. `{t('[word]')}` diff --git a/002_source/cms/src/components/dashboard/cr/categories/_SUMMARY.md b/002_source/cms/src/components/dashboard/cr/categories/_SUMMARY.md new file mode 100644 index 0000000..c0990fc --- /dev/null +++ b/002_source/cms/src/components/dashboard/cr/categories/_SUMMARY.md @@ -0,0 +1,94 @@ +# CR Categories Components Summary + +## Main Components + +### `cr-categories-table.tsx` + +- Displays categories in a table format with columns for Name, Status, Created At, etc. +- Features: + - Row selection functionality + - Status indicators (Active/Blocked/Pending) + - Progress bars for quota/word count + - Edit/delete actions + - Image and name display with slugs + +### `cr-category-create-form.tsx` + +- Form for creating new categories +- Fields: + - Name, image, position, visibility + - Slug, description, remarks + - Initial answer (JSON) +- Uses Zod validation and React Hook Form +- Material UI components +- Internationalization support + +### `cr-category-edit-form.tsx` + +- Similar to create form but for editing +- Pre-fills existing data +- Handles image updates +- More strict validation for init_answer + +## Supporting Components + +### `confirm-delete-modal.tsx` + +- Confirmation dialog for category deletion +- Loading states and toast notifications +- Internationalization support + +### `cr-categories-filters.tsx` + +- Filtering functionality: + - Visibility status tabs + - Text search filters + - Sorting options +- Shows selected items count + +### `cr-categories-pagination.tsx` + +- Basic pagination controls +- Page number and rows per page selection +- Default options: [5, 10, 25] + +### `cr-categories-selection-context.tsx` + +- Manages selection state +- Provides hooks for: + - Selecting/deselecting items + - Checking selection state + - Bulk operations + +## Types & Constants + +### `type.d.ts` + +- Interfaces: + - `CrCategory`: Main category type + - `CreateFormProps`: Create form data + - `EditFormProps`: Edit form data + +### `_constants.ts` + +- Default values: + - `defaultCrCategory` + - `emptyCrCategory` + +## Component Relationships + +```mermaid +graph TD + A[cr-categories-table] --> B[cr-category-create-form] + A --> C[cr-category-edit-form] + A --> D[confirm-delete-modal] + A --> E[cr-categories-filters] + A --> F[cr-categories-pagination] + A --> G[cr-categories-selection-context] + H[type.d.ts] --> A + H --> B + H --> C + I[_constants.ts] --> A + I --> B + I --> C +``` diff --git a/002_source/cms/src/components/dashboard/cr/categories/_constants.ts b/002_source/cms/src/components/dashboard/cr/categories/_constants.ts index 885067e..d530c06 100644 --- a/002_source/cms/src/components/dashboard/cr/categories/_constants.ts +++ b/002_source/cms/src/components/dashboard/cr/categories/_constants.ts @@ -1,8 +1,8 @@ import { dayjs } from '@/lib/dayjs'; -import { CreateFormProps, LpCategory } from './type'; +import { CrCategory, CreateFormProps } from './type'; -export const defaultLpCategory: LpCategory = { +export const defaultCrCategory: CrCategory = { isEmpty: false, id: 'default-id', cat_name: 'default-category-name', @@ -38,7 +38,7 @@ export const defaultLpCategory: LpCategory = { // imageUrl: '', // }; -export const emptyLpCategory: LpCategory = { - ...defaultLpCategory, +export const emptyCrCategory: CrCategory = { + ...defaultCrCategory, isEmpty: true, }; diff --git a/002_source/cms/src/components/dashboard/cr/categories/confirm-delete-modal.tsx b/002_source/cms/src/components/dashboard/cr/categories/confirm-delete-modal.tsx index 4d5b267..f8ec60d 100644 --- a/002_source/cms/src/components/dashboard/cr/categories/confirm-delete-modal.tsx +++ b/002_source/cms/src/components/dashboard/cr/categories/confirm-delete-modal.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { useRouter } from 'next/navigation'; import { COL_LESSON_TYPES } from '@/constants'; -import deleteQuizLPCategories from '@/db/QuizListenings/Delete'; +import deleteQuizCRCategories from '@/db/QuizCRCategories/Delete'; import { LoadingButton } from '@mui/lab'; import { Button, Container, Modal, Paper } from '@mui/material'; import Avatar from '@mui/material/Avatar'; @@ -46,7 +46,9 @@ export default function ConfirmDeleteModal({ function handleUserConfirmDelete(): void { if (idToDelete) { setIsDeleteing(true); - deleteQuizLPCategories(idToDelete) + + // RULES: CR -> deleteQuizCRCategories + deleteQuizCRCategories(idToDelete) .then(() => { reloadRows(); handleClose(); diff --git a/002_source/cms/src/components/dashboard/cr/categories/lp-categories-filters.tsx b/002_source/cms/src/components/dashboard/cr/categories/cr-categories-filters.tsx similarity index 95% rename from 002_source/cms/src/components/dashboard/cr/categories/lp-categories-filters.tsx rename to 002_source/cms/src/components/dashboard/cr/categories/cr-categories-filters.tsx index 7573cc6..3286f93 100644 --- a/002_source/cms/src/components/dashboard/cr/categories/lp-categories-filters.tsx +++ b/002_source/cms/src/components/dashboard/cr/categories/cr-categories-filters.tsx @@ -3,9 +3,13 @@ import * as React from 'react'; import { useRouter } from 'next/navigation'; // import { COL_LESSON_CATEGORIES } from '@/constants'; + +// RULES: Quiz import GetAllCount from '@/db/QuizListenings/GetAllCount'; import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount'; -import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount'; +// import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount'; +// import GetVisibleCount from '@/db/Q'; +// import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; import Divider from '@mui/material/Divider'; @@ -23,8 +27,8 @@ import { paths } from '@/paths'; import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; import { Option } from '@/components/core/option'; -import { useLpCategoriesSelection } from './lp-categories-selection-context'; -import { LpCategory } from './type'; +import { useCrCategoriesSelection } from './cr-categories-selection-context'; +import { CrCategory } from './type'; export interface Filters { email?: string; @@ -37,17 +41,17 @@ export interface Filters { export type SortDir = 'asc' | 'desc'; -export interface LpCategoriesFiltersProps { +export interface CrCategoriesFiltersProps { filters?: Filters; sortDir?: SortDir; - fullData: LpCategory[]; + fullData: CrCategory[]; } -export function LpCategoriesFilters({ +export function CrCategoriesFilters({ filters = {}, sortDir = 'desc', fullData, -}: LpCategoriesFiltersProps): React.JSX.Element { +}: CrCategoriesFiltersProps): React.JSX.Element { const { t } = useTranslation(); const { email, phone, status, name, visible, type } = filters; @@ -57,16 +61,16 @@ export function LpCategoriesFilters({ const router = useRouter(); - const selection = useLpCategoriesSelection(); + const selection = useCrCategoriesSelection(); function getVisible(): number { - return fullData.reduce((count, item: LpCategory) => { + return fullData.reduce((count, item: CrCategory) => { return item.visible === 'visible' ? count + 1 : count; }, 0); } function getHidden(): number { - return fullData.reduce((count, item: LpCategory) => { + return fullData.reduce((count, item: CrCategory) => { return item.visible === 'hidden' ? count + 1 : count; }, 0); } diff --git a/002_source/cms/src/components/dashboard/cr/categories/lp-categories-pagination.tsx b/002_source/cms/src/components/dashboard/cr/categories/cr-categories-pagination.tsx similarity index 89% rename from 002_source/cms/src/components/dashboard/cr/categories/lp-categories-pagination.tsx rename to 002_source/cms/src/components/dashboard/cr/categories/cr-categories-pagination.tsx index 21bdbef..ac75b6e 100644 --- a/002_source/cms/src/components/dashboard/cr/categories/lp-categories-pagination.tsx +++ b/002_source/cms/src/components/dashboard/cr/categories/cr-categories-pagination.tsx @@ -10,7 +10,7 @@ function noop(): void { return undefined; } -interface LpCategoriesPaginationProps { +interface CrCategoriesPaginationProps { count: number; page: number; // @@ -19,14 +19,14 @@ interface LpCategoriesPaginationProps { rowsPerPage: number; } -export function LpCategoriesPagination({ +export function CrCategoriesPagination({ count, page, // setPage, setRowsPerPage, rowsPerPage, -}: LpCategoriesPaginationProps): React.JSX.Element { +}: CrCategoriesPaginationProps): 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) => { diff --git a/002_source/cms/src/components/dashboard/cr/categories/lp-categories-selection-context.tsx b/002_source/cms/src/components/dashboard/cr/categories/cr-categories-selection-context.tsx similarity index 60% rename from 002_source/cms/src/components/dashboard/cr/categories/lp-categories-selection-context.tsx rename to 002_source/cms/src/components/dashboard/cr/categories/cr-categories-selection-context.tsx index a6c6502..fbc2620 100644 --- a/002_source/cms/src/components/dashboard/cr/categories/lp-categories-selection-context.tsx +++ b/002_source/cms/src/components/dashboard/cr/categories/cr-categories-selection-context.tsx @@ -6,15 +6,15 @@ import * as React from 'react'; import { useSelection } from '@/hooks/use-selection'; import type { Selection } from '@/hooks/use-selection'; -import { LpCategory } from './type'; +import { CrCategory } from './type'; function noop(): void { return undefined; } -export interface LpCategoriesSelectionContextValue extends Selection {} +export interface CrCategoriesSelectionContextValue extends Selection {} -export const LpCategoriesSelectionContext = React.createContext({ +export const CrCategoriesSelectionContext = React.createContext({ deselectAll: noop, deselectOne: noop, selectAll: noop, @@ -26,10 +26,10 @@ export const LpCategoriesSelectionContext = React.createContext{children} + {children} ); } -export function useLpCategoriesSelection(): LpCategoriesSelectionContextValue { - return React.useContext(LpCategoriesSelectionContext); +export function useCrCategoriesSelection(): CrCategoriesSelectionContextValue { + return React.useContext(CrCategoriesSelectionContext); } diff --git a/002_source/cms/src/components/dashboard/cr/categories/mf-categories-table.tsx b/002_source/cms/src/components/dashboard/cr/categories/cr-categories-table.tsx similarity index 93% rename from 002_source/cms/src/components/dashboard/cr/categories/mf-categories-table.tsx rename to 002_source/cms/src/components/dashboard/cr/categories/cr-categories-table.tsx index 26034f1..1c342f4 100644 --- a/002_source/cms/src/components/dashboard/cr/categories/mf-categories-table.tsx +++ b/002_source/cms/src/components/dashboard/cr/categories/cr-categories-table.tsx @@ -25,10 +25,10 @@ import { DataTable } from '@/components/core/data-table'; import type { ColumnDef } from '@/components/core/data-table'; import ConfirmDeleteModal from './confirm-delete-modal'; -import { useLpCategoriesSelection } from './lp-categories-selection-context'; -import type { LpCategory } from './type'; +import { useCrCategoriesSelection } from './cr-categories-selection-context'; +import type { CrCategory } from './type'; -function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { +function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { return [ { formatter: (row): React.JSX.Element => ( @@ -40,7 +40,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef @@ -163,7 +163,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef @@ -187,13 +187,13 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef void; } -export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element { +export function CrCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element { const { t } = useTranslation(['lp_categories']); - const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpCategoriesSelection(); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCrCategoriesSelection(); const [idToDelete, setIdToDelete] = React.useState(''); const [open, setOpen] = React.useState(false); @@ -211,7 +211,7 @@ export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTablePro reloadRows={reloadRows} setOpen={setOpen} /> - + columns={columns(handleDeleteClick)} onDeselectAll={deselectAll} onDeselectOne={(_, row) => { @@ -232,6 +232,7 @@ export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTablePro sx={{ textAlign: 'center' }} variant="body2" > + {/* TODO: update this */} {t('no-lesson-categories-found')}
diff --git a/002_source/cms/src/components/dashboard/cr/categories/mf-category-create-form.tsx b/002_source/cms/src/components/dashboard/cr/categories/cr-category-create-form.tsx similarity index 99% rename from 002_source/cms/src/components/dashboard/cr/categories/mf-category-create-form.tsx rename to 002_source/cms/src/components/dashboard/cr/categories/cr-category-create-form.tsx index ba46ac7..319b1dd 100644 --- a/002_source/cms/src/components/dashboard/cr/categories/mf-category-create-form.tsx +++ b/002_source/cms/src/components/dashboard/cr/categories/cr-category-create-form.tsx @@ -66,7 +66,7 @@ export const defaultValues = { description: '', } satisfies Values; -export function LpCategoryCreateForm(): React.JSX.Element { +export function CrCategoryCreateForm(): React.JSX.Element { const router = useRouter(); const { t } = useTranslation(['lp_categories']); diff --git a/002_source/cms/src/components/dashboard/cr/questions/lp-question-edit-form.tsx b/002_source/cms/src/components/dashboard/cr/categories/cr-category-edit-form.tsx similarity index 99% rename from 002_source/cms/src/components/dashboard/cr/questions/lp-question-edit-form.tsx rename to 002_source/cms/src/components/dashboard/cr/categories/cr-category-edit-form.tsx index a38d9b2..4424082 100644 --- a/002_source/cms/src/components/dashboard/cr/questions/lp-question-edit-form.tsx +++ b/002_source/cms/src/components/dashboard/cr/categories/cr-category-edit-form.tsx @@ -88,7 +88,7 @@ const defaultValues = { description: '', } satisfies Values; -export function LpQuestionEditForm(): React.JSX.Element { +export function CrCategoryEditForm(): React.JSX.Element { const router = useRouter(); const { t } = useTranslation(['lp_categories']); @@ -132,7 +132,7 @@ export function LpQuestionEditForm(): React.JSX.Element { const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate); logger.debug(result); toast.success(t('edit.success')); - router.push(paths.dashboard.lp_categories.list); + router.push(paths.dashboard.cr_categories.list); } catch (error) { logger.error(error); toast.error(t('update.failed')); @@ -480,7 +480,7 @@ export function LpQuestionEditForm(): React.JSX.Element { diff --git a/002_source/cms/src/components/dashboard/cr/categories/lp-categories-table.tsx b/002_source/cms/src/components/dashboard/cr/categories/lp-categories-table.tsx deleted file mode 100644 index 26034f1..0000000 --- a/002_source/cms/src/components/dashboard/cr/categories/lp-categories-table.tsx +++ /dev/null @@ -1,241 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { LoadingButton } from '@mui/lab'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import LinearProgress from '@mui/material/LinearProgress'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; -import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images'; -import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; -import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; -import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple'; -import { useTranslation } from 'react-i18next'; -import { toast } from 'sonner'; - -import { paths } from '@/paths'; -import { dayjs } from '@/lib/dayjs'; -import { DataTable } from '@/components/core/data-table'; -import type { ColumnDef } from '@/components/core/data-table'; - -import ConfirmDeleteModal from './confirm-delete-modal'; -import { useLpCategoriesSelection } from './lp-categories-selection-context'; -import type { LpCategory } from './type'; - -function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { - return [ - { - formatter: (row): React.JSX.Element => ( - - - - - - {' '} -
- {row.cat_name} - - slug: {row.cat_name} - -
-
- -
- ), - name: 'Name', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => ( - - - - {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} - - - ), - // NOTE: please refer to translation.json here - name: 'word-count', - width: '100px', - }, - { - formatter: (row): React.JSX.Element => { - // eslint-disable-next-line react-hooks/rules-of-hooks - - const mapping = { - active: { - label: 'Active', - icon: ( - - ), - }, - blocked: { label: 'Blocked', icon: }, - pending: { - label: 'Pending', - icon: ( - - ), - }, - NA: { - label: 'NA', - icon: ( - - ), - }, - } as const; - const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; - - return ( - - ); - }, - name: 'Status', - width: '150px', - }, - { - formatter(row) { - return dayjs(row.createdAt).format('MMM D, YYYY'); - }, - name: 'Created at', - width: '100px', - }, - { - formatter: (row): React.JSX.Element => ( - - - - - { - handleDeleteClick(row.id); - }} - > - - - - ), - name: 'Actions', - hideName: true, - width: '100px', - align: 'right', - }, - ]; -} - -export interface LessonCategoriesTableProps { - rows: LpCategory[]; - reloadRows: () => void; -} - -export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element { - const { t } = useTranslation(['lp_categories']); - const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpCategoriesSelection(); - - const [idToDelete, setIdToDelete] = React.useState(''); - const [open, setOpen] = React.useState(false); - - function handleDeleteClick(testId: string): void { - setOpen(true); - setIdToDelete(testId); - } - - return ( - - - - columns={columns(handleDeleteClick)} - onDeselectAll={deselectAll} - onDeselectOne={(_, row) => { - deselectOne(row.id); - }} - onSelectAll={selectAll} - onSelectOne={(_, row) => { - selectOne(row.id); - }} - rows={rows} - selectable - selected={selected} - /> - {!rows.length ? ( - - - {t('no-lesson-categories-found')} - - - ) : null} - - ); -} diff --git a/002_source/cms/src/components/dashboard/cr/categories/lp-category-create-form.tsx b/002_source/cms/src/components/dashboard/cr/categories/lp-category-create-form.tsx deleted file mode 100644 index ba46ac7..0000000 --- a/002_source/cms/src/components/dashboard/cr/categories/lp-category-create-form.tsx +++ /dev/null @@ -1,419 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useRouter } from 'next/navigation'; -import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { LoadingButton } from '@mui/lab'; -import { Avatar, Divider, MenuItem, Select } from '@mui/material'; -// import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardActions from '@mui/material/CardActions'; -import CardContent from '@mui/material/CardContent'; -import FormControl from '@mui/material/FormControl'; -import FormHelperText from '@mui/material/FormHelperText'; -import InputLabel from '@mui/material/InputLabel'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import Grid from '@mui/material/Unstable_Grid2'; -import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; -// import axios from 'axios'; -import { Controller, useForm } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; -import { z as zod } from 'zod'; - -import { paths } from '@/paths'; -import isDevelopment from '@/lib/check-is-development'; -import { logger } from '@/lib/default-logger'; -import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; -import { pb } from '@/lib/pb'; -import { TextEditor } from '@/components/core/text-editor/text-editor'; -import { toast } from '@/components/core/toaster'; - -import type { CreateFormProps } from './type'; - -const schema = zod.object({ - cat_name: zod.string().min(1, 'name-is-required').max(255), - cat_image: zod.array(zod.any()).optional(), - pos: zod.number().min(1, 'position is required').max(99), - init_answer: zod.string().optional(), - visible: zod.string(), - slug: zod.string().min(1, 'slug-is-required').max(255), - remarks: zod.string().optional(), - description: zod.string().optional(), - // NOTE: for image handling - avatar: zod.string().optional(), - // TODO: remove me - type: zod.string().optional(), - isActive: zod.boolean().optional(), - order: zod.number().optional(), -}); - -type Values = zod.infer; - -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_QUIZ_LP_CATEGORIES).create(payload); - - logger.debug(result); - toast.success(t('create.success')); - router.push(paths.dashboard.lp_categories.list); - } catch (error) { - logger.error(error); - toast.error(t('create.failed')); - } finally { - setIsCreating(false); - } - }, - // t is not necessary here - // eslint-disable-next-line react-hooks/exhaustive-deps - [router] - ); - - const avatarInputRef = React.useRef(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/cr/categories/mf-categories-filters.tsx b/002_source/cms/src/components/dashboard/cr/categories/mf-categories-filters.tsx deleted file mode 100644 index 7573cc6..0000000 --- a/002_source/cms/src/components/dashboard/cr/categories/mf-categories-filters.tsx +++ /dev/null @@ -1,456 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { useRouter } from 'next/navigation'; -// import { COL_LESSON_CATEGORIES } from '@/constants'; -import GetAllCount from '@/db/QuizListenings/GetAllCount'; -import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount'; -import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount'; -import Button from '@mui/material/Button'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Select from '@mui/material/Select'; -import type { SelectChangeEvent } from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Tab from '@mui/material/Tab'; -import Tabs from '@mui/material/Tabs'; -import Typography from '@mui/material/Typography'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; -import { Option } from '@/components/core/option'; - -import { useLpCategoriesSelection } from './lp-categories-selection-context'; -import { LpCategory } from './type'; - -export interface Filters { - email?: string; - phone?: string; - status?: string; - name?: string; - visible?: string; - type?: string; -} - -export type SortDir = 'asc' | 'desc'; - -export interface LpCategoriesFiltersProps { - filters?: Filters; - sortDir?: SortDir; - fullData: LpCategory[]; -} - -export function LpCategoriesFilters({ - filters = {}, - sortDir = 'desc', - fullData, -}: LpCategoriesFiltersProps): React.JSX.Element { - const { t } = useTranslation(); - const { email, phone, status, name, visible, type } = filters; - - const [totalCount, setTotalCount] = React.useState(0); - const [visibleCount, setVisibleCount] = React.useState(0); - const [hiddenCount, setHiddenCount] = React.useState(0); - - const router = useRouter(); - - const selection = useLpCategoriesSelection(); - - function getVisible(): number { - return fullData.reduce((count, item: LpCategory) => { - return item.visible === 'visible' ? count + 1 : count; - }, 0); - } - - function getHidden(): number { - return fullData.reduce((count, item: LpCategory) => { - return item.visible === 'hidden' ? count + 1 : count; - }, 0); - } - - // The tabs should be generated using API data. - const tabs = [ - { label: t('All'), value: '', count: totalCount }, - // { label: 'Active', value: 'active', count: 3 }, - // { label: 'Pending', value: 'pending', count: 1 }, - // { label: 'Blocked', value: 'blocked', count: 1 }, - { label: t('visible'), value: 'visible', count: visibleCount }, - { label: t('hidden'), value: 'hidden', count: hiddenCount }, - ] as const; - - const updateSearchParams = React.useCallback( - (newFilters: Filters, newSortDir: SortDir): void => { - const searchParams = new URLSearchParams(); - - if (newSortDir === 'asc') { - searchParams.set('sortDir', newSortDir); - } - - if (newFilters.status) { - searchParams.set('status', newFilters.status); - } - - if (newFilters.email) { - searchParams.set('email', newFilters.email); - } - - if (newFilters.phone) { - searchParams.set('phone', newFilters.phone); - } - - if (newFilters.name) { - searchParams.set('name', newFilters.name); - } - - if (newFilters.type) { - searchParams.set('type', newFilters.type); - } - - if (newFilters.visible) { - searchParams.set('visible', newFilters.visible); - } - - // NOTE: modify according to COLLECTION - router.push(`${paths.dashboard.lp_categories.list}?${searchParams.toString()}`); - }, - [router] - ); - - const handleClearFilters = React.useCallback(() => { - updateSearchParams({}, sortDir); - }, [updateSearchParams, sortDir]); - - const handleStatusChange = React.useCallback( - (_: React.SyntheticEvent, value: string) => { - updateSearchParams({ ...filters, status: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleVisibleChange = React.useCallback( - (_: React.SyntheticEvent, value: string) => { - updateSearchParams({ ...filters, visible: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleNameChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, name: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleTypeChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, type: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleEmailChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, email: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handlePhoneChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, phone: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleSortChange = React.useCallback( - (event: SelectChangeEvent) => { - updateSearchParams(filters, event.target.value as SortDir); - }, - [updateSearchParams, filters] - ); - - React.useEffect(() => { - const fetchCount = async (): Promise => { - try { - const tc = await GetAllCount(); - setTotalCount(tc); - - const vc = await GetVisibleCount(); - setVisibleCount(vc); - - const hc = await GetHiddenCount(); - setHiddenCount(hc); - } catch (error) { - // - } - }; - void fetchCount(); - }, []); - - const hasFilters = status || email || phone || visible || name || type; - - return ( -
- - {tabs.map((tab) => ( - - } - iconPosition="end" - key={tab.value} - label={tab.label} - sx={{ minHeight: 'auto' }} - tabIndex={0} - value={tab.value} - /> - ))} - - - - - { - handleNameChange(value as string); - }} - onFilterDelete={() => { - handleNameChange(); - }} - popover={} - value={name} - /> - - { - handleTypeChange(value as string); - }} - onFilterDelete={() => { - handleTypeChange(); - }} - popover={} - value={type} - /> - - {hasFilters ? : null} - - {selection.selectedAny ? ( - - - {selection.selected.size} {t('selected')} - - - - ) : null} - - -
- ); -} - -function TypeFilterPopover(): React.JSX.Element { - const { t } = useTranslation(); - const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); - const [value, setValue] = React.useState(''); - - React.useEffect(() => { - setValue((initialValue as string | undefined) ?? ''); - }, [initialValue]); - - return ( - - - { - setValue(event.target.value); - }} - onKeyUp={(event) => { - if (event.key === 'Enter') { - onApply(value); - } - }} - value={value} - /> - - - - ); -} - -function NameFilterPopover(): React.JSX.Element { - const { t } = useTranslation(); - const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); - const [value, setValue] = React.useState(''); - - React.useEffect(() => { - setValue((initialValue as string | undefined) ?? ''); - }, [initialValue]); - - return ( - - - { - setValue(event.target.value); - }} - onKeyUp={(event) => { - if (event.key === 'Enter') { - onApply(value); - } - }} - value={value} - /> - - - - ); -} - -function EmailFilterPopover(): React.JSX.Element { - const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); - const [value, setValue] = React.useState(''); - const { t } = useTranslation(); - - React.useEffect(() => { - setValue((initialValue as string | undefined) ?? ''); - }, [initialValue]); - - return ( - - - { - setValue(event.target.value); - }} - onKeyUp={(event) => { - if (event.key === 'Enter') { - onApply(value); - } - }} - value={value} - /> - - - - ); -} - -function PhoneFilterPopover(): React.JSX.Element { - const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); - const [value, setValue] = React.useState(''); - const { t } = useTranslation(); - - React.useEffect(() => { - setValue((initialValue as string | undefined) ?? ''); - }, [initialValue]); - - return ( - - - { - setValue(event.target.value); - }} - onKeyUp={(event) => { - if (event.key === 'Enter') { - onApply(value); - } - }} - value={value} - /> - - - - ); -} diff --git a/002_source/cms/src/components/dashboard/cr/categories/mf-categories-pagination.tsx b/002_source/cms/src/components/dashboard/cr/categories/mf-categories-pagination.tsx deleted file mode 100644 index bf40481..0000000 --- a/002_source/cms/src/components/dashboard/cr/categories/mf-categories-pagination.tsx +++ /dev/null @@ -1,49 +0,0 @@ -'use client'; - -import * as React from 'react'; -import TablePagination from '@mui/material/TablePagination'; - -function noop(): void { - return undefined; -} - -interface LessonCategoriesPaginationProps { - count: number; - page: number; - // - setPage: (page: number) => void; - setRowsPerPage: (page: number) => void; - rowsPerPage: number; -} - -export function LpCategoriesPagination({ - count, - page, - // - setPage, - setRowsPerPage, - rowsPerPage, -}: LessonCategoriesPaginationProps): React.JSX.Element { - // You should implement the pagination using a similar logic as the filters. - // Note that when page change, you should keep the filter search params. - const handleChangePage = (event: unknown, newPage: number) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event: React.ChangeEvent) => { - setRowsPerPage(parseInt(event.target.value)); - // console.log(parseInt(event.target.value)); - }; - - return ( - - ); -} diff --git a/002_source/cms/src/components/dashboard/cr/categories/mf-categories-selection-context.tsx b/002_source/cms/src/components/dashboard/cr/categories/mf-categories-selection-context.tsx deleted file mode 100644 index a6c6502..0000000 --- a/002_source/cms/src/components/dashboard/cr/categories/mf-categories-selection-context.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client'; - -import * as React from 'react'; - -// import type { LessonCategory } from '@/types/lesson-type'; -import { useSelection } from '@/hooks/use-selection'; -import type { Selection } from '@/hooks/use-selection'; - -import { LpCategory } from './type'; - -function noop(): void { - return undefined; -} - -export interface LpCategoriesSelectionContextValue extends Selection {} - -export const LpCategoriesSelectionContext = React.createContext({ - deselectAll: noop, - deselectOne: noop, - selectAll: noop, - selectOne: noop, - selected: new Set(), - selectedAny: false, - selectedAll: false, -}); - -interface LpCategoriesSelectionProviderProps { - children: React.ReactNode; - lessonCategories: LpCategory[]; -} - -export function LpCategoriesSelectionProvider({ - children, - lessonCategories = [], -}: LpCategoriesSelectionProviderProps): React.JSX.Element { - const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]); - const selection = useSelection(customerIds); - - return ( - {children} - ); -} - -export function useLpCategoriesSelection(): LpCategoriesSelectionContextValue { - return React.useContext(LpCategoriesSelectionContext); -} diff --git a/002_source/cms/src/components/dashboard/cr/categories/mf-category-edit-form.tsx b/002_source/cms/src/components/dashboard/cr/categories/mf-category-edit-form.tsx deleted file mode 100644 index c00e964..0000000 --- a/002_source/cms/src/components/dashboard/cr/categories/mf-category-edit-form.tsx +++ /dev/null @@ -1,500 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -// -import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { LoadingButton } from '@mui/lab'; -// -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardActions from '@mui/material/CardActions'; -import CardContent from '@mui/material/CardContent'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import FormHelperText from '@mui/material/FormHelperText'; -import InputLabel from '@mui/material/InputLabel'; -import MenuItem from '@mui/material/MenuItem'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Select from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import Grid from '@mui/material/Unstable_Grid2'; -// -import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; -// -import { Controller, useForm } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; -import { z as zod } from 'zod'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; -import { pb } from '@/lib/pb'; -import { TextEditor } from '@/components/core/text-editor/text-editor'; -import { toast } from '@/components/core/toaster'; -import FormLoading from '@/components/loading'; - -import ErrorDisplay from '../../error'; -import type { EditFormProps } from './type'; - -// TODO: review this -const schema = zod.object({ - cat_name: zod.string().min(1, 'name-is-required').max(255), - // accept file object when user change image - // accept http string when user not changing image - cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(), - - // position - pos: zod.number().min(1, 'position is required').max(99), - - // it should be a valid JSON - init_answer: zod - .string() - .refine( - (value) => { - try { - JSON.parse(value); - return true; - } catch (error) { - return false; - } - }, - { message: 'init_answer must be a valid JSON' } - ) - .optional(), - visible: zod.string(), - slug: zod.string().min(0, 'slug-is-required').max(255).optional(), - remarks: zod.string().optional(), - description: zod.string().optional(), - // NOTE: for image handling - avatar: zod.string().optional(), -}); - -type Values = zod.infer; - -const defaultValues = { - cat_name: '', - cat_image: undefined, - pos: 1, - init_answer: JSON.stringify({}), - visible: 'hidden', - slug: '', - remarks: '', - description: '', -} satisfies Values; - -export function LpCategoryEditForm(): React.JSX.Element { - const router = useRouter(); - const { t } = useTranslation(['lp_categories']); - - const { cat_id: catId } = useParams<{ cat_id: string }>(); - // - const [isUpdating, setIsUpdating] = React.useState(false); - const [showLoading, setShowLoading] = React.useState(false); - // - const [showError, setShowError] = React.useState({ show: false, detail: '' }); - - const { - control, - handleSubmit, - formState: { errors }, - setValue, - reset, - watch, - } = useForm({ defaultValues, resolver: zodResolver(schema) }); - - const onSubmit = React.useCallback( - async (values: Values): Promise => { - setIsUpdating(true); - - const tempUpdate: EditFormProps = { - cat_name: values.cat_name, - cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null, - pos: values.pos, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - init_answer: JSON.parse(values.init_answer || '{}'), - - visible: values.visible, - slug: values.slug || 'not-defined', - remarks: values.remarks, - description: values.description, - // - // TODO: remove below - type: '', - }; - - try { - const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate); - logger.debug(result); - toast.success(t('edit.success')); - router.push(paths.dashboard.lp_categories.list); - } catch (error) { - logger.error(error); - toast.error(t('update.failed')); - } finally { - setIsUpdating(false); - } - }, - // t is not necessary here - // eslint-disable-next-line react-hooks/exhaustive-deps - [router] - ); - - const avatarInputRef = React.useRef(null); - const avatar = watch('avatar'); - - const handleAvatarChange = React.useCallback( - async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - - if (file) { - const url = await fileToBase64(file); - setValue('avatar', url); - } - }, - [setValue] - ); - - // TODO: need to align with save form - // use trycatch - const [textDescription, setTextDescription] = React.useState(''); - const [textRemarks, setTextRemarks] = React.useState(''); - - // load existing data when user arrive - const loadExistingData = React.useCallback( - async (id: string) => { - setShowLoading(true); - - try { - const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); - - reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); - setTextDescription(result.description); - setTextRemarks(result.remarks); - - if (result.cat_image !== '') { - const fetchResult = await fetch( - `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}` - ); - - const blob = await fetchResult.blob(); - const url = await fileToBase64(blob); - - setValue('avatar', url); - } else { - setValue('avatar', ''); - } - } catch (error) { - logger.error(error); - toast(t('list.error')); - - setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); - } finally { - setShowLoading(false); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [catId] - ); - - React.useEffect(() => { - void loadExistingData(catId); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [catId]); - - if (showLoading) return ; - if (showError.show) - return ( - - ); - - return ( -
- - - } - spacing={4} - > - - {t('edit.basic-info')} - - - - - - - - - - {t('edit.avatar')} - {t('edit.avatarRequirements')} - - - - - - - ( - - {t('edit.cat_name')} - - {errors.cat_name ? {errors.cat_name.message} : null} - - )} - /> - - {/* */} - - ( - - {t('edit.pos')} - { - field.onChange(parseInt(e.target.value)); - }} - type="number" - /> - {errors.pos ? {errors.pos.message} : null} - - )} - /> - - {/* */} - - ( - - {t('edit.slug')} - - {errors.slug ? {errors.slug.message} : null} - - )} - /> - - {/* */} - - ( - - {t('edit.visible')} - - - {errors.visible ? {errors.visible.message} : null} - - )} - /> - - {/* */} - - ( - - {t('edit.init_answer')} - - {errors.init_answer ? {errors.init_answer.message} : null} - - )} - /> - - - - {/* */} - - {t('edit.detail-information')} - - - { - return ( - - - {t('edit.description')} - - - { - field.onChange({ target: { value: editor.getHTML() } }); - }} - placeholder={t('edit.description.default')} - /> - - - ); - }} - /> - - - ( - - - {t('edit.remarks')} - - - { - field.onChange({ target: { value: editor.getText() } }); - }} - hideToolbar - placeholder={t('edit.remarks.default')} - /> - - - )} - /> - - - - {/* */} - - - - - - - {t('edit.updateButton')} - - - -
- ); -} diff --git a/002_source/cms/src/components/dashboard/cr/categories/type.d.ts b/002_source/cms/src/components/dashboard/cr/categories/type.d.ts index c66a11f..40fed5e 100644 --- a/002_source/cms/src/components/dashboard/cr/categories/type.d.ts +++ b/002_source/cms/src/components/dashboard/cr/categories/type.d.ts @@ -1,4 +1,4 @@ -export interface LpCategory { +export interface CrCategory { isEmpty?: boolean; // id: string; diff --git a/002_source/cms/src/components/dashboard/cr/questions/_PROMPT.MD b/002_source/cms/src/components/dashboard/cr/questions/_PROMPT.MD deleted file mode 100644 index 369cdca..0000000 --- a/002_source/cms/src/components/dashboard/cr/questions/_PROMPT.MD +++ /dev/null @@ -1,21 +0,0 @@ -please review and add translations, e.g. `{t('[word]')}` - ---- - -please help to review the `tsx` file in this folder -`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/questions` - -it was clone from -`category`/`categories`, `lp_category`/`lp_categories` -please help to modify to `question`/`questions`, `lp_question`/`lp_questions` - -please also help to modify the name of -`variables`, `constants`, `functions`, `classes`, components's name, paths - -the db fields structures are the same - -do not move the files -do not create directories -keep current folder structure is important - -thanks diff --git a/002_source/cms/src/components/dashboard/cr/questions/_SUMMARY.md b/002_source/cms/src/components/dashboard/cr/questions/_SUMMARY.md new file mode 100644 index 0000000..e69de29 diff --git a/002_source/cms/src/components/dashboard/cr/questions/_constants.ts b/002_source/cms/src/components/dashboard/cr/questions/_constants.ts index 95a4d17..4c6d940 100644 --- a/002_source/cms/src/components/dashboard/cr/questions/_constants.ts +++ b/002_source/cms/src/components/dashboard/cr/questions/_constants.ts @@ -1,8 +1,8 @@ import { dayjs } from '@/lib/dayjs'; -import { CreateFormProps, LpQuestion } from './type'; +import { CreateFormProps, CrQuestion } from './type'; -export const defaultLpQuestion: LpQuestion = { +export const defaultCrQuestion: CrQuestion = { isEmpty: false, id: 'default-id', cat_name: 'default-question-name', @@ -38,7 +38,7 @@ export const defaultLpQuestion: LpQuestion = { // imageUrl: '', // }; -export const emptyLpQuestion: LpQuestion = { - ...defaultLpQuestion, +export const emptyLpQuestion: CrQuestion = { + ...defaultCrQuestion, isEmpty: true, }; diff --git a/002_source/cms/src/components/dashboard/cr/questions/lp-question-create-form.tsx b/002_source/cms/src/components/dashboard/cr/questions/cr-question-create-form.tsx similarity index 99% rename from 002_source/cms/src/components/dashboard/cr/questions/lp-question-create-form.tsx rename to 002_source/cms/src/components/dashboard/cr/questions/cr-question-create-form.tsx index a39ad69..7428b04 100644 --- a/002_source/cms/src/components/dashboard/cr/questions/lp-question-create-form.tsx +++ b/002_source/cms/src/components/dashboard/cr/questions/cr-question-create-form.tsx @@ -66,7 +66,7 @@ export const defaultValues = { description: '', } satisfies Values; -export function LpQuestionCreateForm(): React.JSX.Element { +export function CrQuestionCreateForm(): React.JSX.Element { const router = useRouter(); const { t } = useTranslation(['lp_categories']); diff --git a/002_source/cms/src/components/dashboard/cr/categories/lp-category-edit-form.tsx b/002_source/cms/src/components/dashboard/cr/questions/cr-question-edit-form.tsx similarity index 99% rename from 002_source/cms/src/components/dashboard/cr/categories/lp-category-edit-form.tsx rename to 002_source/cms/src/components/dashboard/cr/questions/cr-question-edit-form.tsx index c00e964..eda3e7d 100644 --- a/002_source/cms/src/components/dashboard/cr/categories/lp-category-edit-form.tsx +++ b/002_source/cms/src/components/dashboard/cr/questions/cr-question-edit-form.tsx @@ -88,7 +88,7 @@ const defaultValues = { description: '', } satisfies Values; -export function LpCategoryEditForm(): React.JSX.Element { +export function CrQuestionEditForm(): React.JSX.Element { const router = useRouter(); const { t } = useTranslation(['lp_categories']); diff --git a/002_source/cms/src/components/dashboard/cr/questions/lp-questions-filters.tsx b/002_source/cms/src/components/dashboard/cr/questions/cr-questions-filters.tsx similarity index 97% rename from 002_source/cms/src/components/dashboard/cr/questions/lp-questions-filters.tsx rename to 002_source/cms/src/components/dashboard/cr/questions/cr-questions-filters.tsx index d5b5486..7a71bc7 100644 --- a/002_source/cms/src/components/dashboard/cr/questions/lp-questions-filters.tsx +++ b/002_source/cms/src/components/dashboard/cr/questions/cr-questions-filters.tsx @@ -23,8 +23,8 @@ import { paths } from '@/paths'; import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; import { Option } from '@/components/core/option'; -import { useLpQuestionsSelection } from './lp-questions-selection-context'; -import { LpQuestion } from './type'; +import { useLpQuestionsSelection } from './cr-questions-selection-context'; +import { CrQuestion } from './type'; export interface Filters { email?: string; @@ -40,10 +40,10 @@ export type SortDir = 'asc' | 'desc'; export interface LpQuestionsFiltersProps { filters?: Filters; sortDir?: SortDir; - fullData: LpQuestion[]; + fullData: CrQuestion[]; } -export function LpQuestionsFilters({ +export function CrQuestionsFilters({ filters = {}, sortDir = 'desc', fullData, @@ -60,13 +60,13 @@ export function LpQuestionsFilters({ const selection = useLpQuestionsSelection(); function getVisible(): number { - return fullData.reduce((count, item: LpQuestion) => { + return fullData.reduce((count, item: CrQuestion) => { return item.visible === 'visible' ? count + 1 : count; }, 0); } function getHidden(): number { - return fullData.reduce((count, item: LpQuestion) => { + return fullData.reduce((count, item: CrQuestion) => { return item.visible === 'hidden' ? count + 1 : count; }, 0); } diff --git a/002_source/cms/src/components/dashboard/cr/questions/lp-questions-pagination.tsx b/002_source/cms/src/components/dashboard/cr/questions/cr-questions-pagination.tsx similarity index 96% rename from 002_source/cms/src/components/dashboard/cr/questions/lp-questions-pagination.tsx rename to 002_source/cms/src/components/dashboard/cr/questions/cr-questions-pagination.tsx index deef393..5b91186 100644 --- a/002_source/cms/src/components/dashboard/cr/questions/lp-questions-pagination.tsx +++ b/002_source/cms/src/components/dashboard/cr/questions/cr-questions-pagination.tsx @@ -16,7 +16,7 @@ interface LessonQuestionsPaginationProps { rowsPerPage: number; } -export function LpQuestionsPagination({ +export function CrQuestionsPagination({ count, page, // diff --git a/002_source/cms/src/components/dashboard/cr/questions/lp-questions-selection-context.tsx b/002_source/cms/src/components/dashboard/cr/questions/cr-questions-selection-context.tsx similarity index 90% rename from 002_source/cms/src/components/dashboard/cr/questions/lp-questions-selection-context.tsx rename to 002_source/cms/src/components/dashboard/cr/questions/cr-questions-selection-context.tsx index 8d98b5a..d5dcad1 100644 --- a/002_source/cms/src/components/dashboard/cr/questions/lp-questions-selection-context.tsx +++ b/002_source/cms/src/components/dashboard/cr/questions/cr-questions-selection-context.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { useSelection } from '@/hooks/use-selection'; import type { Selection } from '@/hooks/use-selection'; -import type { LpQuestion } from './type'; +import type { CrQuestion } from './type'; function noop(): void { return undefined; @@ -26,10 +26,10 @@ export const LpQuestionsSelectionContext = React.createContext void): ColumnDef[] { +function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { return [ { formatter: (row): React.JSX.Element => ( @@ -187,11 +187,11 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef void; } -export function LpQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps): React.JSX.Element { +export function CrQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps): React.JSX.Element { const { t } = useTranslation(['lp_categories']); const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpQuestionsSelection(); @@ -211,7 +211,7 @@ export function LpQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps reloadRows={reloadRows} setOpen={setOpen} /> - + columns={columns(handleDeleteClick)} onDeselectAll={deselectAll} onDeselectOne={(_, row) => { @@ -232,6 +232,7 @@ export function LpQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps sx={{ textAlign: 'center' }} variant="body2" > + {/* TODO: update this */} {t('no-lesson-categories-found')} diff --git a/002_source/cms/src/components/dashboard/cr/questions/type.d.ts b/002_source/cms/src/components/dashboard/cr/questions/type.d.ts index 8975fb4..e98a02f 100644 --- a/002_source/cms/src/components/dashboard/cr/questions/type.d.ts +++ b/002_source/cms/src/components/dashboard/cr/questions/type.d.ts @@ -1,4 +1,4 @@ -export interface LpQuestion { +export interface CrQuestion { isEmpty?: boolean; // id: string; diff --git a/002_source/cms/src/components/dashboard/cr/repomix-output.xml b/002_source/cms/src/components/dashboard/cr/repomix-output.xml new file mode 100644 index 0000000..0a4a9a7 --- /dev/null +++ b/002_source/cms/src/components/dashboard/cr/repomix-output.xml @@ -0,0 +1,4642 @@ +This file is a merged representation of the entire codebase, combined into a single document by Repomix. + + +This section contains a summary of this file. + + +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + + + +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Repository files, each consisting of: + - File path as an attribute + - Full contents of the file + + + +- This file should be treated as read-only. Any changes should be made to the + original repository files, not this packed version. +- When processing this file, use the file path to distinguish + between different files in the repository. +- Be aware that this file may contain sensitive information. Handle it with + the same level of security as you would the original repository. + + + +- Some files may have been excluded based on .gitignore rules and Repomix's configuration +- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files +- Files matching patterns in .gitignore are excluded +- Files matching default ignore patterns are excluded +- Files are sorted by Git change count (files with more changes are at the bottom) + + + + + + + + + +categories/ + _constants.ts + _PROMPT.MD + confirm-delete-modal.tsx + cr-categories-filters.tsx + cr-categories-pagination.tsx + cr-categories-selection-context.tsx + cr-categories-table.tsx + cr-category-create-form.tsx + cr-category-edit-form.tsx + notifications.tsx + payments.tsx + shipping-address.tsx + type.d.ts +questions/ + _constants.ts + _PROMPT.MD + confirm-delete-modal.tsx + cr-question-create-form.tsx + cr-question-edit-form.tsx + cr-questions-filters.tsx + cr-questions-pagination.tsx + cr-questions-selection-context.tsx + cr-questions-table.tsx + notifications.tsx + payments.tsx + shipping-address.tsx + type.d.ts + + + +This section contains the contents of the repository's files. + + +import { dayjs } from '@/lib/dayjs'; + +import { CrCategory, CreateFormProps } from './type'; + +export const defaultCrCategory: CrCategory = { + isEmpty: false, + id: 'default-id', + cat_name: 'default-category-name', + cat_image_url: undefined, + cat_image: undefined, + pos: 0, + visible: 'hidden', + lesson_id: 'default-lesson-id', + description: 'default-description', + remarks: 'default-remarks', + slug: '', + init_answer: {}, + // from pocketbase + collectionId: '0000000000', + createdAt: dayjs('2099-01-01').toDate(), + // + name: '', + avatar: '', + email: '', + phone: '', + quota: 0, + status: 'NA', +}; + +// export const LpCategoryCreateFormDefault: CreateFormProps = { +// name: '', +// type: '', +// pos: 1, +// visible: 'visible', +// description: '', +// isActive: true, +// order: 1, +// imageUrl: '', +// }; + +export const emptyCrCategory: CrCategory = { + ...defaultCrCategory, + isEmpty: true, +}; + + + +please review and update translations + + + +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_TYPES } from '@/constants'; +import deleteQuizCRCategories from '@/db/QuizCRCategories/Delete'; +import { LoadingButton } from '@mui/lab'; +import { Button, Container, Modal, Paper } from '@mui/material'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note'; +import PocketBase from 'pocketbase'; +import { useTranslation } from 'react-i18next'; + +import { logger } from '@/lib/default-logger'; +import { toast } from '@/components/core/toaster'; + +export default function ConfirmDeleteModal({ + open, + setOpen, + idToDelete, + reloadRows, +}: { + open: boolean; + setOpen: (b: boolean) => void; + idToDelete: string; + reloadRows: () => void; +}): React.JSX.Element { + const { t } = useTranslation(); + + // const handleClose = () => setOpen(false); + function handleClose(): void { + setOpen(false); + } + + const [isDeleteing, setIsDeleteing] = React.useState(false); + const style = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + }; + + function handleUserConfirmDelete(): void { + if (idToDelete) { + setIsDeleteing(true); + + // RULES: CR -> deleteQuizCRCategories + deleteQuizCRCategories(idToDelete) + .then(() => { + reloadRows(); + handleClose(); + toast(t('delete.success')); + }) + .catch((err) => { + // console.error(err) + logger.error(err); + toast(t('delete.error')); + }) + .finally(() => { + setIsDeleteing(false); + }); + } + } + + return ( +
+ + + + + + + + + + + {t('Delete Lesson Type ?')} + + {t('Are you sure you want to delete lesson type ?')} + + + + + { + handleUserConfirmDelete(); + }} + loading={isDeleteing} + > + {t('Delete')} + + + + + + + + +
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +// import { COL_LESSON_CATEGORIES } from '@/constants'; + +// RULES: Quiz +import GetAllCount from '@/db/QuizListenings/GetAllCount'; +import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount'; +// import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount'; +// import GetVisibleCount from '@/db/Q'; +// +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import type { SelectChangeEvent } from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; +import { Option } from '@/components/core/option'; + +import { useCrCategoriesSelection } from './cr-categories-selection-context'; +import { CrCategory } from './type'; + +export interface Filters { + email?: string; + phone?: string; + status?: string; + name?: string; + visible?: string; + type?: string; +} + +export type SortDir = 'asc' | 'desc'; + +export interface CrCategoriesFiltersProps { + filters?: Filters; + sortDir?: SortDir; + fullData: CrCategory[]; +} + +export function CrCategoriesFilters({ + filters = {}, + sortDir = 'desc', + fullData, +}: CrCategoriesFiltersProps): React.JSX.Element { + const { t } = useTranslation(); + const { email, phone, status, name, visible, type } = filters; + + const [totalCount, setTotalCount] = React.useState(0); + const [visibleCount, setVisibleCount] = React.useState(0); + const [hiddenCount, setHiddenCount] = React.useState(0); + + const router = useRouter(); + + const selection = useCrCategoriesSelection(); + + function getVisible(): number { + return fullData.reduce((count, item: CrCategory) => { + return item.visible === 'visible' ? count + 1 : count; + }, 0); + } + + function getHidden(): number { + return fullData.reduce((count, item: CrCategory) => { + return item.visible === 'hidden' ? count + 1 : count; + }, 0); + } + + // The tabs should be generated using API data. + const tabs = [ + { label: t('All'), value: '', count: totalCount }, + // { label: 'Active', value: 'active', count: 3 }, + // { label: 'Pending', value: 'pending', count: 1 }, + // { label: 'Blocked', value: 'blocked', count: 1 }, + { label: t('visible'), value: 'visible', count: visibleCount }, + { label: t('hidden'), value: 'hidden', count: hiddenCount }, + ] as const; + + const updateSearchParams = React.useCallback( + (newFilters: Filters, newSortDir: SortDir): void => { + const searchParams = new URLSearchParams(); + + if (newSortDir === 'asc') { + searchParams.set('sortDir', newSortDir); + } + + if (newFilters.status) { + searchParams.set('status', newFilters.status); + } + + if (newFilters.email) { + searchParams.set('email', newFilters.email); + } + + if (newFilters.phone) { + searchParams.set('phone', newFilters.phone); + } + + if (newFilters.name) { + searchParams.set('name', newFilters.name); + } + + if (newFilters.type) { + searchParams.set('type', newFilters.type); + } + + if (newFilters.visible) { + searchParams.set('visible', newFilters.visible); + } + + // NOTE: modify according to COLLECTION + router.push(`${paths.dashboard.lp_categories.list}?${searchParams.toString()}`); + }, + [router] + ); + + const handleClearFilters = React.useCallback(() => { + updateSearchParams({}, sortDir); + }, [updateSearchParams, sortDir]); + + const handleStatusChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, status: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleVisibleChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, visible: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleNameChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, name: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleTypeChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, type: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleEmailChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, email: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handlePhoneChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, phone: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleSortChange = React.useCallback( + (event: SelectChangeEvent) => { + updateSearchParams(filters, event.target.value as SortDir); + }, + [updateSearchParams, filters] + ); + + React.useEffect(() => { + const fetchCount = async (): Promise => { + try { + const tc = await GetAllCount(); + setTotalCount(tc); + + const vc = await GetVisibleCount(); + setVisibleCount(vc); + + const hc = await GetHiddenCount(); + setHiddenCount(hc); + } catch (error) { + // + } + }; + void fetchCount(); + }, []); + + const hasFilters = status || email || phone || visible || name || type; + + return ( +
+ + {tabs.map((tab) => ( + + } + iconPosition="end" + key={tab.value} + label={tab.label} + sx={{ minHeight: 'auto' }} + tabIndex={0} + value={tab.value} + /> + ))} + + + + + { + handleNameChange(value as string); + }} + onFilterDelete={() => { + handleNameChange(); + }} + popover={} + value={name} + /> + + { + handleTypeChange(value as string); + }} + onFilterDelete={() => { + handleTypeChange(); + }} + popover={} + value={type} + /> + + {hasFilters ? : null} + + {selection.selectedAny ? ( + + + {selection.selected.size} {t('selected')} + + + + ) : null} + + +
+ ); +} + +function TypeFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function NameFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function EmailFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function PhoneFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} +
+ + +'use client'; + +// lp-categories-pagination.tsx +// RULES: +// T.B.A. +import * as React from 'react'; +import TablePagination from '@mui/material/TablePagination'; + +function noop(): void { + return undefined; +} + +interface CrCategoriesPaginationProps { + count: number; + page: number; + // + setPage: (page: number) => void; + setRowsPerPage: (page: number) => void; + rowsPerPage: number; +} + +export function CrCategoriesPagination({ + count, + page, + // + setPage, + setRowsPerPage, + rowsPerPage, +}: CrCategoriesPaginationProps): React.JSX.Element { + // You should implement the pagination using a similar logic as the filters. + // Note that when page change, you should keep the filter search params. + const handleChangePage = (event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value)); + // console.log(parseInt(event.target.value)); + }; + + return ( + + ); +} + + + +'use client'; + +import * as React from 'react'; + +// import type { LessonCategory } from '@/types/lesson-type'; +import { useSelection } from '@/hooks/use-selection'; +import type { Selection } from '@/hooks/use-selection'; + +import { CrCategory } from './type'; + +function noop(): void { + return undefined; +} + +export interface CrCategoriesSelectionContextValue extends Selection {} + +export const CrCategoriesSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface LpCategoriesSelectionProviderProps { + children: React.ReactNode; + lessonCategories: CrCategory[]; +} + +export function CrCategoriesSelectionProvider({ + children, + lessonCategories = [], +}: LpCategoriesSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]); + const selection = useSelection(customerIds); + + return ( + {children} + ); +} + +export function useCrCategoriesSelection(): CrCategoriesSelectionContextValue { + return React.useContext(CrCategoriesSelectionContext); +} + + + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { LoadingButton } from '@mui/lab'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; +import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images'; +import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; + +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; + +import ConfirmDeleteModal from './confirm-delete-modal'; +import { useCrCategoriesSelection } from './cr-categories-selection-context'; +import type { CrCategory } from './type'; + +function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { + return [ + { + formatter: (row): React.JSX.Element => ( + + + + + + {' '} +
+ {row.cat_name} + + slug: {row.cat_name} + +
+
+ +
+ ), + name: 'Name', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} + + + ), + // NOTE: please refer to translation.json here + name: 'word-count', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => { + // eslint-disable-next-line react-hooks/rules-of-hooks + + const mapping = { + active: { + label: 'Active', + icon: ( + + ), + }, + blocked: { label: 'Blocked', icon: }, + pending: { + label: 'Pending', + icon: ( + + ), + }, + NA: { + label: 'NA', + icon: ( + + ), + }, + } as const; + const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; + + return ( + + ); + }, + name: 'Status', + width: '150px', + }, + { + formatter(row) { + return dayjs(row.createdAt).format('MMM D, YYYY'); + }, + name: 'Created at', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + + { + handleDeleteClick(row.id); + }} + > + + + + ), + name: 'Actions', + hideName: true, + width: '100px', + align: 'right', + }, + ]; +} + +export interface LessonCategoriesTableProps { + rows: CrCategory[]; + reloadRows: () => void; +} + +export function CrCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element { + const { t } = useTranslation(['lp_categories']); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCrCategoriesSelection(); + + const [idToDelete, setIdToDelete] = React.useState(''); + const [open, setOpen] = React.useState(false); + + function handleDeleteClick(testId: string): void { + setOpen(true); + setIdToDelete(testId); + } + + return ( + + + + columns={columns(handleDeleteClick)} + onDeselectAll={deselectAll} + onDeselectOne={(_, row) => { + deselectOne(row.id); + }} + onSelectAll={selectAll} + onSelectOne={(_, row) => { + selectOne(row.id); + }} + rows={rows} + selectable + selected={selected} + /> + {!rows.length ? ( + + + {/* TODO: update this */} + {t('no-lesson-categories-found')} + + + ) : null} + + ); +} +
+ + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +import { Avatar, Divider, MenuItem, Select } from '@mui/material'; +// import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// import axios from 'axios'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import isDevelopment from '@/lib/check-is-development'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; + +import type { CreateFormProps } from './type'; + +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + cat_image: zod.array(zod.any()).optional(), + pos: zod.number().min(1, 'position is required').max(99), + init_answer: zod.string().optional(), + visible: zod.string(), + slug: zod.string().min(1, 'slug-is-required').max(255), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), + // TODO: remove me + type: zod.string().optional(), + isActive: zod.boolean().optional(), + order: zod.number().optional(), +}); + +type Values = zod.infer; + +export const defaultValues = { + cat_name: '', + cat_image: undefined, + pos: 1, + init_answer: '', + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function CrCategoryCreateForm(): 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_QUIZ_LP_CATEGORIES).create(payload); + + logger.debug(result); + toast.success(t('create.success')); + router.push(paths.dashboard.lp_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('create.failed')); + } finally { + setIsCreating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(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)}
+
+
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +// +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +// +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +// +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; +import FormLoading from '@/components/loading'; + +import ErrorDisplay from '../../error'; +import type { EditFormProps } from './type'; + +// TODO: review this +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + // accept file object when user change image + // accept http string when user not changing image + cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(), + + // position + pos: zod.number().min(1, 'position is required').max(99), + + // it should be a valid JSON + init_answer: zod + .string() + .refine( + (value) => { + try { + JSON.parse(value); + return true; + } catch (error) { + return false; + } + }, + { message: 'init_answer must be a valid JSON' } + ) + .optional(), + visible: zod.string(), + slug: zod.string().min(0, 'slug-is-required').max(255).optional(), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), +}); + +type Values = zod.infer; + +const defaultValues = { + cat_name: '', + cat_image: undefined, + pos: 1, + init_answer: JSON.stringify({}), + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function CrCategoryEditForm(): React.JSX.Element { + const router = useRouter(); + 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, + handleSubmit, + formState: { errors }, + setValue, + reset, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + setIsUpdating(true); + + const tempUpdate: EditFormProps = { + cat_name: values.cat_name, + cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null, + pos: values.pos, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + init_answer: JSON.parse(values.init_answer || '{}'), + + visible: values.visible, + slug: values.slug || 'not-defined', + remarks: values.remarks, + description: values.description, + // + // TODO: remove below + type: '', + }; + + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate); + logger.debug(result); + toast.success(t('edit.success')); + router.push(paths.dashboard.cr_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('update.failed')); + } finally { + setIsUpdating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(null); + const avatar = watch('avatar'); + + const handleAvatarChange = React.useCallback( + async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + + if (file) { + const url = await fileToBase64(file); + setValue('avatar', url); + } + }, + [setValue] + ); + + // TODO: need to align with save form + // use trycatch + const [textDescription, setTextDescription] = React.useState(''); + const [textRemarks, setTextRemarks] = React.useState(''); + + // load existing data when user arrive + const loadExistingData = React.useCallback( + async (id: string) => { + setShowLoading(true); + + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); + + reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); + setTextDescription(result.description); + setTextRemarks(result.remarks); + + if (result.cat_image !== '') { + const fetchResult = await fetch( + `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}` + ); + + const blob = await fetchResult.blob(); + const url = await fileToBase64(blob); + + setValue('avatar', url); + } else { + setValue('avatar', ''); + } + } catch (error) { + logger.error(error); + toast(t('list.error')); + + setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); + } finally { + setShowLoading(false); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [catId] + ); + + React.useEffect(() => { + void loadExistingData(catId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [catId]); + + if (showLoading) return ; + if (showError.show) + return ( + + ); + + return ( +
+ + + } + spacing={4} + > + + {t('edit.basic-info')} + + + + + + + + + + {t('edit.avatar')} + {t('edit.avatarRequirements')} + + + + + + + ( + + {t('edit.cat_name')} + + {errors.cat_name ? {errors.cat_name.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.pos')} + { + field.onChange(parseInt(e.target.value)); + }} + type="number" + /> + {errors.pos ? {errors.pos.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.slug')} + + {errors.slug ? {errors.slug.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.visible')} + + + {errors.visible ? {errors.visible.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.init_answer')} + + {errors.init_answer ? {errors.init_answer.message} : null} + + )} + /> + + + + {/* */} + + {t('edit.detail-information')} + + + { + return ( + + + {t('edit.description')} + + + { + field.onChange({ target: { value: editor.getHTML() } }); + }} + placeholder={t('edit.description.default')} + /> + + + ); + }} + /> + + + ( + + + {t('edit.remarks')} + + + { + field.onChange({ target: { value: editor.getText() } }); + }} + hideToolbar + placeholder={t('edit.remarks.default')} + /> + + + )} + /> + + + + {/* */} + + + + + + + {t('edit.updateButton')} + + + +
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; + +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; +import { Option } from '@/components/core/option'; + +export interface Notification { + id: string; + type: string; + status: 'delivered' | 'pending' | 'failed'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {row.type} + + ), + name: 'Type', + width: '300px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + delivered: { label: 'Delivered', color: 'success' }, + pending: { label: 'Pending', color: 'warning' }, + failed: { label: 'Failed', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface NotificationsProps { + notifications: Notification[]; +} + +export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { + return ( + + + + + } + title="Notifications" + /> + + + + +
+ +
+
+ + + columns={columns} rows={notifications} /> + + +
+
+
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple'; + +import { dayjs } from '@/lib/dayjs'; +import type { ColumnDef } from '@/components/core/data-table'; +import { DataTable } from '@/components/core/data-table'; + +export interface Payment { + currency: string; + amount: number; + invoiceId: string; + status: 'pending' | 'completed' | 'canceled' | 'refunded'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)} + + ), + name: 'Amount', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + pending: { label: 'Pending', color: 'warning' }, + completed: { label: 'Completed', color: 'success' }, + canceled: { label: 'Canceled', color: 'error' }, + refunded: { label: 'Refunded', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + return {row.invoiceId}; + }, + name: 'Invoice ID', + width: '150px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface PaymentsProps { + ordersValue: number; + payments: Payment[]; + refundsValue: number; + totalOrders: number; +} + +export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { + return ( + + }> + Create Payment + + } + avatar={ + + + + } + title="Payments" + /> + + + + } + spacing={3} + sx={{ justifyContent: 'space-between', p: 2 }} + > +
+ + Total orders + + {new Intl.NumberFormat('en-US').format(totalOrders)} +
+
+ + Orders value + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} + +
+
+ + Refunds + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} + +
+
+
+ + + columns={columns} rows={payments} /> + + +
+
+
+ ); +} +
+ + +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; + +export interface Address { + id: string; + country: string; + state: string; + city: string; + zipCode: string; + street: string; + primary?: boolean; +} + +export interface ShippingAddressProps { + address: Address; +} + +export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement { + return ( + + + + + {address.street}, +
+ {address.city}, {address.state}, {address.country}, +
+ {address.zipCode} +
+ + {address.primary ? : } + + +
+
+
+ ); +} +
+ + +export interface CrCategory { + isEmpty?: boolean; + // + id: string; + collectionId: string; + // + cat_name: string; + cat_image_url?: string; + cat_image?: string; + pos: number; + visible: string; + lesson_id: string; + description: string; + remarks: string; + slug: string; + init_answer: any; + // + name: string; + avatar: string; + email: string; + phone: string; + quota: number; + status: 'pending' | 'active' | 'blocked' | 'NA'; + createdAt: Date; +} + +export interface CreateFormProps { + cat_name: string; + cat_image: File[] | null; + pos: number; + init_answer?: string; + visible: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: to remove + type: string; + isActive: boolean; + order: number; + name?: string; + imageUrl?: string; +} + +export interface EditFormProps { + cat_name: string; + cat_image: File[] | null; + pos: number; + init_answer: any; + visible: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: remove below + type: string; +} + +export interface Helloworld { + helloworld: string; +} + + + +import { dayjs } from '@/lib/dayjs'; + +import { CreateFormProps, LpQuestion } from './type'; + +export const defaultLpQuestion: LpQuestion = { + isEmpty: false, + id: 'default-id', + cat_name: 'default-question-name', + cat_image_url: undefined, + cat_image: undefined, + pos: 0, + visible: 'hidden', + lesson_id: 'default-lesson-id', + description: 'default-description', + remarks: 'default-remarks', + slug: '', + init_answer: {}, + // from pocketbase + collectionId: '0000000000', + createdAt: dayjs('2099-01-01').toDate(), + // + name: '', + avatar: '', + email: '', + phone: '', + quota: 0, + status: 'NA', +}; + +// export const LpCategoryCreateFormDefault: CreateFormProps = { +// name: '', +// type: '', +// pos: 1, +// visible: 'visible', +// description: '', +// isActive: true, +// order: 1, +// imageUrl: '', +// }; + +export const emptyLpQuestion: LpQuestion = { + ...defaultLpQuestion, + isEmpty: true, +}; + + + +please review and add translations, e.g. `{t('[word]')}` + +--- + +please help to review the `tsx` file in this folder +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/questions` + +it was clone from +`category`/`categories`, `lp_category`/`lp_categories` +please help to modify to `question`/`questions`, `lp_question`/`lp_questions` + +please also help to modify the name of +`variables`, `constants`, `functions`, `classes`, components's name, paths + +the db fields structures are the same + +do not move the files +do not create directories +keep current folder structure is important + +thanks + + + +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_TYPES } from '@/constants'; +import deleteQuizLPCategories from '@/db/QuizListenings/Delete'; +import { LoadingButton } from '@mui/lab'; +import { Button, Container, Modal, Paper } from '@mui/material'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note'; +import PocketBase from 'pocketbase'; +import { useTranslation } from 'react-i18next'; + +import { logger } from '@/lib/default-logger'; +import { toast } from '@/components/core/toaster'; + +export default function ConfirmDeleteModal({ + open, + setOpen, + idToDelete, + reloadRows, +}: { + open: boolean; + setOpen: (b: boolean) => void; + idToDelete: string; + reloadRows: () => void; +}): React.JSX.Element { + const { t } = useTranslation(); + + // const handleClose = () => setOpen(false); + function handleClose(): void { + setOpen(false); + } + + const [isDeleteing, setIsDeleteing] = React.useState(false); + const style = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + }; + + function handleUserConfirmDelete(): void { + if (idToDelete) { + setIsDeleteing(true); + deleteQuizLPCategories(idToDelete) + .then(() => { + reloadRows(); + handleClose(); + toast(t('delete.success')); + }) + .catch((err) => { + // console.error(err) + logger.error(err); + toast(t('delete.error')); + }) + .finally(() => { + setIsDeleteing(false); + }); + } + } + + return ( +
+ + + + + + + + + + + {t('Delete Lesson Type ?')} + + {t('Are you sure you want to delete lesson type ?')} + + + + + { + handleUserConfirmDelete(); + }} + loading={isDeleteing} + > + {t('Delete')} + + + + + + + + +
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +import { Avatar, Divider, MenuItem, Select } from '@mui/material'; +// import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// import axios from 'axios'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import isDevelopment from '@/lib/check-is-development'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; + +import type { CreateFormProps } from './type'; + +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + cat_image: zod.array(zod.any()).optional(), + pos: zod.number().min(1, 'position is required').max(99), + init_answer: zod.string().optional(), + visible: zod.string(), + slug: zod.string().min(1, 'slug-is-required').max(255), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), + // TODO: remove me + type: zod.string().optional(), + isActive: zod.boolean().optional(), + order: zod.number().optional(), +}); + +type Values = zod.infer; + +export const defaultValues = { + cat_name: '', + cat_image: undefined, + pos: 1, + init_answer: '', + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function LpQuestionCreateForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['lp_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_QUIZ_LP_CATEGORIES).create(payload); + + logger.debug(result); + toast.success(t('create.success')); + router.push(paths.dashboard.lp_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('create.failed')); + } finally { + setIsCreating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(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)}
+
+
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +// +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +// +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +// +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; +import FormLoading from '@/components/loading'; + +import ErrorDisplay from '../../error'; +import type { EditFormProps } from './type'; + +// TODO: review this +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + // accept file object when user change image + // accept http string when user not changing image + cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(), + + // position + pos: zod.number().min(1, 'position is required').max(99), + + // it should be a valid JSON + init_answer: zod + .string() + .refine( + (value) => { + try { + JSON.parse(value); + return true; + } catch (error) { + return false; + } + }, + { message: 'init_answer must be a valid JSON' } + ) + .optional(), + visible: zod.string(), + slug: zod.string().min(0, 'slug-is-required').max(255).optional(), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), +}); + +type Values = zod.infer; + +const defaultValues = { + cat_name: '', + cat_image: undefined, + pos: 1, + init_answer: JSON.stringify({}), + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function LpQuestionEditForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['lp_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, + handleSubmit, + formState: { errors }, + setValue, + reset, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + setIsUpdating(true); + + const tempUpdate: EditFormProps = { + cat_name: values.cat_name, + cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null, + pos: values.pos, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + init_answer: JSON.parse(values.init_answer || '{}'), + + visible: values.visible, + slug: values.slug || 'not-defined', + remarks: values.remarks, + description: values.description, + // + // TODO: remove below + type: '', + }; + + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate); + logger.debug(result); + toast.success(t('edit.success')); + router.push(paths.dashboard.lp_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('update.failed')); + } finally { + setIsUpdating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(null); + const avatar = watch('avatar'); + + const handleAvatarChange = React.useCallback( + async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + + if (file) { + const url = await fileToBase64(file); + setValue('avatar', url); + } + }, + [setValue] + ); + + // TODO: need to align with save form + // use trycatch + const [textDescription, setTextDescription] = React.useState(''); + const [textRemarks, setTextRemarks] = React.useState(''); + + // load existing data when user arrive + const loadExistingData = React.useCallback( + async (id: string) => { + setShowLoading(true); + + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); + + reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); + setTextDescription(result.description); + setTextRemarks(result.remarks); + + if (result.cat_image !== '') { + const fetchResult = await fetch( + `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}` + ); + + const blob = await fetchResult.blob(); + const url = await fileToBase64(blob); + + setValue('avatar', url); + } else { + setValue('avatar', ''); + } + } catch (error) { + logger.error(error); + toast(t('list.error')); + + setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); + } finally { + setShowLoading(false); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [catId] + ); + + React.useEffect(() => { + void loadExistingData(catId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [catId]); + + if (showLoading) return ; + if (showError.show) + return ( + + ); + + return ( +
+ + + } + spacing={4} + > + + {t('edit.basic-info')} + + + + + + + + + + {t('edit.avatar')} + {t('edit.avatarRequirements')} + + + + + + + ( + + {t('edit.cat_name')} + + {errors.cat_name ? {errors.cat_name.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.pos')} + { + field.onChange(parseInt(e.target.value)); + }} + type="number" + /> + {errors.pos ? {errors.pos.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.slug')} + + {errors.slug ? {errors.slug.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.visible')} + + + {errors.visible ? {errors.visible.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.init_answer')} + + {errors.init_answer ? {errors.init_answer.message} : null} + + )} + /> + + + + {/* */} + + {t('edit.detail-information')} + + + { + return ( + + + {t('edit.description')} + + + { + field.onChange({ target: { value: editor.getHTML() } }); + }} + placeholder={t('edit.description.default')} + /> + + + ); + }} + /> + + + ( + + + {t('edit.remarks')} + + + { + field.onChange({ target: { value: editor.getText() } }); + }} + hideToolbar + placeholder={t('edit.remarks.default')} + /> + + + )} + /> + + + + {/* */} + + + + + + + {t('edit.updateButton')} + + + +
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +// import { COL_LESSON_CATEGORIES } from '@/constants'; +import GetAllCount from '@/db/QuizListenings/GetAllCount'; +import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount'; +import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount'; +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import type { SelectChangeEvent } from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; +import { Option } from '@/components/core/option'; + +import { useLpQuestionsSelection } from './cr-questions-selection-context'; +import { LpQuestion } from './type'; + +export interface Filters { + email?: string; + phone?: string; + status?: string; + name?: string; + visible?: string; + type?: string; +} + +export type SortDir = 'asc' | 'desc'; + +export interface LpQuestionsFiltersProps { + filters?: Filters; + sortDir?: SortDir; + fullData: LpQuestion[]; +} + +export function LpQuestionsFilters({ + filters = {}, + sortDir = 'desc', + fullData, +}: LpQuestionsFiltersProps): React.JSX.Element { + const { t } = useTranslation(); + const { email, phone, status, name, visible, type } = filters; + + const [totalCount, setTotalCount] = React.useState(0); + const [visibleCount, setVisibleCount] = React.useState(0); + const [hiddenCount, setHiddenCount] = React.useState(0); + + const router = useRouter(); + + const selection = useLpQuestionsSelection(); + + function getVisible(): number { + return fullData.reduce((count, item: LpQuestion) => { + return item.visible === 'visible' ? count + 1 : count; + }, 0); + } + + function getHidden(): number { + return fullData.reduce((count, item: LpQuestion) => { + return item.visible === 'hidden' ? count + 1 : count; + }, 0); + } + + // The tabs should be generated using API data. + const tabs = [ + { label: t('All'), value: '', count: totalCount }, + // { label: 'Active', value: 'active', count: 3 }, + // { label: 'Pending', value: 'pending', count: 1 }, + // { label: 'Blocked', value: 'blocked', count: 1 }, + { label: t('visible'), value: 'visible', count: visibleCount }, + { label: t('hidden'), value: 'hidden', count: hiddenCount }, + ] as const; + + const updateSearchParams = React.useCallback( + (newFilters: Filters, newSortDir: SortDir): void => { + const searchParams = new URLSearchParams(); + + if (newSortDir === 'asc') { + searchParams.set('sortDir', newSortDir); + } + + if (newFilters.status) { + searchParams.set('status', newFilters.status); + } + + if (newFilters.email) { + searchParams.set('email', newFilters.email); + } + + if (newFilters.phone) { + searchParams.set('phone', newFilters.phone); + } + + if (newFilters.name) { + searchParams.set('name', newFilters.name); + } + + if (newFilters.type) { + searchParams.set('type', newFilters.type); + } + + if (newFilters.visible) { + searchParams.set('visible', newFilters.visible); + } + + // NOTE: modify according to COLLECTION + router.push(`${paths.dashboard.lp_questions.list}?${searchParams.toString()}`); + }, + [router] + ); + + const handleClearFilters = React.useCallback(() => { + updateSearchParams({}, sortDir); + }, [updateSearchParams, sortDir]); + + const handleStatusChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, status: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleVisibleChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, visible: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleNameChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, name: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleTypeChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, type: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleEmailChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, email: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handlePhoneChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, phone: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleSortChange = React.useCallback( + (event: SelectChangeEvent) => { + updateSearchParams(filters, event.target.value as SortDir); + }, + [updateSearchParams, filters] + ); + + React.useEffect(() => { + const fetchCount = async (): Promise => { + try { + const tc = await GetAllCount(); + setTotalCount(tc); + + const vc = await GetVisibleCount(); + setVisibleCount(vc); + + const hc = await GetHiddenCount(); + setHiddenCount(hc); + } catch (error) { + // + } + }; + void fetchCount(); + }, []); + + const hasFilters = status || email || phone || visible || name || type; + + return ( +
+ + {tabs.map((tab) => ( + + } + iconPosition="end" + key={tab.value} + label={tab.label} + sx={{ minHeight: 'auto' }} + tabIndex={0} + value={tab.value} + /> + ))} + + + + + { + handleNameChange(value as string); + }} + onFilterDelete={() => { + handleNameChange(); + }} + popover={} + value={name} + /> + + { + handleTypeChange(value as string); + }} + onFilterDelete={() => { + handleTypeChange(); + }} + popover={} + value={type} + /> + + {hasFilters ? : null} + + {selection.selectedAny ? ( + + + {selection.selected.size} {t('selected')} + + + + ) : null} + + +
+ ); +} + +function TypeFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function NameFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function EmailFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function PhoneFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} +
+ + +'use client'; + +import * as React from 'react'; +import TablePagination from '@mui/material/TablePagination'; + +function noop(): void { + return undefined; +} + +interface LessonQuestionsPaginationProps { + count: number; + page: number; + // + setPage: (page: number) => void; + setRowsPerPage: (page: number) => void; + rowsPerPage: number; +} + +export function LpQuestionsPagination({ + count, + page, + // + setPage, + setRowsPerPage, + rowsPerPage, +}: LessonQuestionsPaginationProps): React.JSX.Element { + // You should implement the pagination using a similar logic as the filters. + // Note that when page change, you should keep the filter search params. + const handleChangePage = (event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value)); + // console.log(parseInt(event.target.value)); + }; + + return ( + + ); +} + + + +'use client'; + +import * as React from 'react'; + +// import type { LessonCategory } from '@/types/lesson-type'; +import { useSelection } from '@/hooks/use-selection'; +import type { Selection } from '@/hooks/use-selection'; + +import type { LpQuestion } from './type'; + +function noop(): void { + return undefined; +} + +export interface LpQuestionsSelectionContextValue extends Selection {} + +export const LpQuestionsSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface LpQuestionsSelectionProviderProps { + children: React.ReactNode; + lessonQuestions: LpQuestion[]; +} + +export function LpQuestionsSelectionProvider({ + children, + lessonQuestions = [], +}: LpQuestionsSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => lessonQuestions.map((customer) => customer.id), [lessonQuestions]); + const selection = useSelection(customerIds); + + return ( + {children} + ); +} + +export function useLpQuestionsSelection(): LpQuestionsSelectionContextValue { + return React.useContext(LpQuestionsSelectionContext); +} + + + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { LoadingButton } from '@mui/lab'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; +import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images'; +import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; + +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; + +import ConfirmDeleteModal from './confirm-delete-modal'; +import { useLpQuestionsSelection } from './cr-questions-selection-context'; +import type { LpQuestion } from './type'; + +function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { + return [ + { + formatter: (row): React.JSX.Element => ( + + + + + + {' '} +
+ {row.cat_name} + + slug: {row.cat_name} + +
+
+ +
+ ), + name: 'Name', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} + + + ), + // NOTE: please refer to translation.json here + name: 'word-count', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => { + // eslint-disable-next-line react-hooks/rules-of-hooks + + const mapping = { + active: { + label: 'Active', + icon: ( + + ), + }, + blocked: { label: 'Blocked', icon: }, + pending: { + label: 'Pending', + icon: ( + + ), + }, + NA: { + label: 'NA', + icon: ( + + ), + }, + } as const; + const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; + + return ( + + ); + }, + name: 'Status', + width: '150px', + }, + { + formatter(row) { + return dayjs(row.createdAt).format('MMM D, YYYY'); + }, + name: 'Created at', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + + { + handleDeleteClick(row.id); + }} + > + + + + ), + name: 'Actions', + hideName: true, + width: '100px', + align: 'right', + }, + ]; +} + +export interface LessonQuestionsTableProps { + rows: LpQuestion[]; + reloadRows: () => void; +} + +export function LpQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps): React.JSX.Element { + const { t } = useTranslation(['lp_categories']); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpQuestionsSelection(); + + const [idToDelete, setIdToDelete] = React.useState(''); + const [open, setOpen] = React.useState(false); + + function handleDeleteClick(testId: string): void { + setOpen(true); + setIdToDelete(testId); + } + + return ( + + + + columns={columns(handleDeleteClick)} + onDeselectAll={deselectAll} + onDeselectOne={(_, row) => { + deselectOne(row.id); + }} + onSelectAll={selectAll} + onSelectOne={(_, row) => { + selectOne(row.id); + }} + rows={rows} + selectable + selected={selected} + /> + {!rows.length ? ( + + + {/* TODO: update this */} + {t('no-lesson-categories-found')} + + + ) : null} + + ); +} +
+ + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; + +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; +import { Option } from '@/components/core/option'; + +export interface Notification { + id: string; + type: string; + status: 'delivered' | 'pending' | 'failed'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {row.type} + + ), + name: 'Type', + width: '300px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + delivered: { label: 'Delivered', color: 'success' }, + pending: { label: 'Pending', color: 'warning' }, + failed: { label: 'Failed', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface NotificationsProps { + notifications: Notification[]; +} + +export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { + return ( + + + + + } + title="Notifications" + /> + + + + +
+ +
+
+ + + columns={columns} rows={notifications} /> + + +
+
+
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple'; + +import { dayjs } from '@/lib/dayjs'; +import type { ColumnDef } from '@/components/core/data-table'; +import { DataTable } from '@/components/core/data-table'; + +export interface Payment { + currency: string; + amount: number; + invoiceId: string; + status: 'pending' | 'completed' | 'canceled' | 'refunded'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)} + + ), + name: 'Amount', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + pending: { label: 'Pending', color: 'warning' }, + completed: { label: 'Completed', color: 'success' }, + canceled: { label: 'Canceled', color: 'error' }, + refunded: { label: 'Refunded', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + return {row.invoiceId}; + }, + name: 'Invoice ID', + width: '150px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface PaymentsProps { + ordersValue: number; + payments: Payment[]; + refundsValue: number; + totalOrders: number; +} + +export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { + return ( + + }> + Create Payment + + } + avatar={ + + + + } + title="Payments" + /> + + + + } + spacing={3} + sx={{ justifyContent: 'space-between', p: 2 }} + > +
+ + Total orders + + {new Intl.NumberFormat('en-US').format(totalOrders)} +
+
+ + Orders value + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} + +
+
+ + Refunds + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} + +
+
+
+ + + columns={columns} rows={payments} /> + + +
+
+
+ ); +} +
+ + +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; + +export interface Address { + id: string; + country: string; + state: string; + city: string; + zipCode: string; + street: string; + primary?: boolean; +} + +export interface ShippingAddressProps { + address: Address; +} + +export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement { + return ( + + + + + {address.street}, +
+ {address.city}, {address.state}, {address.country}, +
+ {address.zipCode} +
+ + {address.primary ? : } + + +
+
+
+ ); +} +
+ + +export interface LpQuestion { + isEmpty?: boolean; + // + id: string; + collectionId: string; + // + cat_name: string; + cat_image_url?: string; + cat_image?: string; + pos: number; + visible: string; + lesson_id: string; + description: string; + remarks: string; + slug: string; + init_answer: any; + // + name: string; + avatar: string; + email: string; + phone: string; + quota: number; + status: 'pending' | 'active' | 'blocked' | 'NA'; + createdAt: Date; +} + +export interface CreateFormProps { + cat_name: string; + cat_image: File[] | null; + pos: number; + init_answer?: string; + visible: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: to remove + type: string; + isActive: boolean; + order: number; + name?: string; + imageUrl?: string; +} + +export interface EditFormProps { + cat_name: string; + cat_image: File[] | null; + pos: number; + init_answer: any; + visible: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: remove below + type: string; +} + +export interface Helloworld { + helloworld: string; +} + + +
diff --git a/002_source/cms/src/components/dashboard/lp/categories/_SUMMARY.md b/002_source/cms/src/components/dashboard/lp/categories/_SUMMARY.md new file mode 100644 index 0000000..02d4f6a --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp/categories/_SUMMARY.md @@ -0,0 +1,111 @@ +# LP Categories Components Summary + +## Main Components + +### `lp-categories-table.tsx` + +- Displays LP categories in a table format with columns for: + - Name with image + - Slug + - Status indicators + - Created date + - Action buttons +- Features: + - Single and multiple row selection + - Status indicators (Active/Inactive) + - Edit/view/delete actions + - Integration with other LP components + +### `lp-category-create-form.tsx` + +- Form for creating new LP categories +- Key fields: + - Name (required) + - Image upload + - Slug (auto-generated) + - Status toggle + - Description (rich text) + - Additional metadata +- Features: + - Form validation + - Image handling + - Auto-slug generation + - Internationalization support + +### `lp-category-edit-form.tsx` + +- Form for editing existing LP categories +- Similar to create form but: + - Pre-populates existing data + - Handles image updates differently + - May have additional edit-specific validation + +## Supporting Components + +### `confirm-delete-modal.tsx` + +- Confirmation dialog for LP category deletion +- Shows: + - Delete confirmation message + - Loading state during deletion + - Success/error notifications + +### `lp-categories-filters.tsx` + +- Filtering controls for LP categories table +- Includes: + - Status filter tabs + - Text search + - Sort options + - Selected items count + - Bulk actions + +### `lp-categories-pagination.tsx` + +- Pagination controls for LP categories table +- Standard features: + - Page navigation + - Rows per page selection + - Current/total page display + +### `lp-categories-selection-context.tsx` + +- Manages selection state for LP categories +- Provides: + - Selection/deselection functions + - Current selection state + - Bulk operation support + +## Types & Constants + +### `type.d.ts` + +- Type definitions including: + - `LpCategory`: Main LP category type + - Form props for create/edit + - Component-specific prop types + +### `_constants.ts` + +- Contains: + - Default LP category values + - Empty category template + - Other shared constants + +## Component Relationships + +```mermaid +graph TD + A[lp-categories-table] --> B[lp-category-create-form] + A --> C[lp-category-edit-form] + A --> D[confirm-delete-modal] + A --> E[lp-categories-filters] + A --> F[lp-categories-pagination] + A --> G[lp-categories-selection-context] + H[type.d.ts] --> A + H --> B + H --> C + I[_constants.ts] --> A + I --> B + I --> C +``` diff --git a/002_source/cms/src/components/dashboard/lp/questions/_SUMMARY.md b/002_source/cms/src/components/dashboard/lp/questions/_SUMMARY.md new file mode 100644 index 0000000..2d77306 --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp/questions/_SUMMARY.md @@ -0,0 +1,86 @@ +# LP Questions Components Summary + +## Main Components + +### LP Questions Table + +- Primary data display component using Material UI DataTable +- Features: + - Column configurations with custom formatters + - Status indicators with visual icons + - Progress bars for quota visualization + - Row selection and bulk operations + - Internationalization support + - Integration with filters and pagination + +### Question Forms + +- **Create Form**: + + - React Hook Form with Zod schema validation + - Image upload with preview functionality + - Rich text editors for descriptions + - Internationalized labels and error messages + - Form submission to PocketBase + +- **Edit Form**: + - Pre-fills existing data from PocketBase + - Conditional image handling + - Loading states and error handling + - JSON validation for complex fields + - Pre-filled rich text editors + +## Supporting Components + +### Selection Management + +- Context-based selection state +- Supports: + - Individual selection/deselection + - Bulk select/deselect operations + - Selection state tracking (selectedAny, selectedAll) + - Integration with question IDs + +### Filters & Pagination + +- **Filters**: + + - Tab-based filtering (All/Visible/Hidden) with counts + - Name and type search functionality + - Sort controls (Newest/Oldest) + - URL parameter synchronization + +- **Pagination**: + - Material UI TablePagination implementation + - Standard page size options + - Callback-based integration with parent + +### Delete Confirmation + +- Modal dialog with: + - Loading state during deletion + - Success/error toast notifications + - Internationalized messages + - PocketBase integration + - Custom error logging + +## Types & Constants + +- Type definitions for LP question data (LpQuestion) +- Shared constants for: + - Column configurations + - Form defaults + - Validation rules + +## Component Relationships + +```mermaid +graph TD + A[LP Questions Table] --> B[Question Forms] + A --> C[Selection Context] + A --> D[Filters] + A --> E[Pagination] + A --> F[Delete Modal] + B --> G[PocketBase] + F --> G +``` diff --git a/002_source/cms/src/components/dashboard/lp/questions/lp-questions-table.tsx b/002_source/cms/src/components/dashboard/lp/questions/lp-questions-table.tsx index e78356c..319ff9c 100644 --- a/002_source/cms/src/components/dashboard/lp/questions/lp-questions-table.tsx +++ b/002_source/cms/src/components/dashboard/lp/questions/lp-questions-table.tsx @@ -232,6 +232,7 @@ export function LpQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps sx={{ textAlign: 'center' }} variant="body2" > + {/* TODO: update this */} {t('no-lesson-categories-found')} diff --git a/002_source/cms/src/components/dashboard/lp/repomix-output.xml b/002_source/cms/src/components/dashboard/lp/repomix-output.xml new file mode 100644 index 0000000..9c97ce2 --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp/repomix-output.xml @@ -0,0 +1,6370 @@ +This file is a merged representation of the entire codebase, combined into a single document by Repomix. + + +This section contains a summary of this file. + + +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + + + +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Repository files, each consisting of: + - File path as an attribute + - Full contents of the file + + + +- This file should be treated as read-only. Any changes should be made to the + original repository files, not this packed version. +- When processing this file, use the file path to distinguish + between different files in the repository. +- Be aware that this file may contain sensitive information. Handle it with + the same level of security as you would the original repository. + + + +- Some files may have been excluded based on .gitignore rules and Repomix's configuration +- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files +- Files matching patterns in .gitignore are excluded +- Files matching default ignore patterns are excluded +- Files are sorted by Git change count (files with more changes are at the bottom) + + + + + + + + + +categories/ + _constants.ts + _PROMPT.MD + confirm-delete-modal.tsx + lp-categories-filters.tsx + lp-categories-pagination.tsx + lp-categories-selection-context.tsx + lp-categories-table.tsx + lp-category-create-form.tsx + lp-category-edit-form.tsx + mf-categories-filters.tsx + mf-categories-pagination.tsx + mf-categories-selection-context.tsx + mf-categories-table.tsx + mf-category-create-form.tsx + mf-category-edit-form.tsx + notifications.tsx + payments.tsx + shipping-address.tsx + type.d.ts +questions/ + _constants.ts + _PROMPT.MD + confirm-delete-modal.tsx + lp-question-create-form.tsx + lp-question-edit-form.tsx + lp-questions-filters.tsx + lp-questions-pagination.tsx + lp-questions-selection-context.tsx + lp-questions-table.tsx + notifications.tsx + payments.tsx + shipping-address.tsx + type.d.ts + + + +This section contains the contents of the repository's files. + + +import { dayjs } from '@/lib/dayjs'; + +import { CreateFormProps, LpCategory } from './type'; + +export const defaultLpCategory: LpCategory = { + isEmpty: false, + id: 'default-id', + cat_name: 'default-category-name', + cat_image_url: undefined, + cat_image: undefined, + pos: 0, + visible: 'hidden', + lesson_id: 'default-lesson-id', + description: 'default-description', + remarks: 'default-remarks', + slug: '', + init_answer: {}, + // from pocketbase + collectionId: '0000000000', + createdAt: dayjs('2099-01-01').toDate(), + // + name: '', + avatar: '', + email: '', + phone: '', + quota: 0, + status: 'NA', +}; + +// export const LpCategoryCreateFormDefault: CreateFormProps = { +// name: '', +// type: '', +// pos: 1, +// visible: 'visible', +// description: '', +// isActive: true, +// order: 1, +// imageUrl: '', +// }; + +export const emptyLpCategory: LpCategory = { + ...defaultLpCategory, + isEmpty: true, +}; + + + +please review and add translations, e.g. `{t('[word]')}` + + + +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_TYPES } from '@/constants'; +import deleteQuizLPCategories from '@/db/QuizListenings/Delete'; +import { LoadingButton } from '@mui/lab'; +import { Button, Container, Modal, Paper } from '@mui/material'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note'; +import PocketBase from 'pocketbase'; +import { useTranslation } from 'react-i18next'; + +import { logger } from '@/lib/default-logger'; +import { toast } from '@/components/core/toaster'; + +export default function ConfirmDeleteModal({ + open, + setOpen, + idToDelete, + reloadRows, +}: { + open: boolean; + setOpen: (b: boolean) => void; + idToDelete: string; + reloadRows: () => void; +}): React.JSX.Element { + const { t } = useTranslation(); + + // const handleClose = () => setOpen(false); + function handleClose(): void { + setOpen(false); + } + + const [isDeleteing, setIsDeleteing] = React.useState(false); + const style = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + }; + + function handleUserConfirmDelete(): void { + if (idToDelete) { + setIsDeleteing(true); + deleteQuizLPCategories(idToDelete) + .then(() => { + reloadRows(); + handleClose(); + toast(t('delete.success')); + }) + .catch((err) => { + // console.error(err) + logger.error(err); + toast(t('delete.error')); + }) + .finally(() => { + setIsDeleteing(false); + }); + } + } + + return ( +
+ + + + + + + + + + + {t('Delete Lesson Type ?')} + + {t('Are you sure you want to delete lesson type ?')} + + + + + { + handleUserConfirmDelete(); + }} + loading={isDeleteing} + > + {t('Delete')} + + + + + + + + +
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +// import { COL_LESSON_CATEGORIES } from '@/constants'; +import GetAllCount from '@/db/QuizListenings/GetAllCount'; +import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount'; +import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount'; +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import type { SelectChangeEvent } from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; +import { Option } from '@/components/core/option'; + +import { useLpCategoriesSelection } from './lp-categories-selection-context'; +import { LpCategory } from './type'; + +export interface Filters { + email?: string; + phone?: string; + status?: string; + name?: string; + visible?: string; + type?: string; +} + +export type SortDir = 'asc' | 'desc'; + +export interface LpCategoriesFiltersProps { + filters?: Filters; + sortDir?: SortDir; + fullData: LpCategory[]; +} + +export function LpCategoriesFilters({ + filters = {}, + sortDir = 'desc', + fullData, +}: LpCategoriesFiltersProps): React.JSX.Element { + const { t } = useTranslation(); + const { email, phone, status, name, visible, type } = filters; + + const [totalCount, setTotalCount] = React.useState(0); + const [visibleCount, setVisibleCount] = React.useState(0); + const [hiddenCount, setHiddenCount] = React.useState(0); + + const router = useRouter(); + + const selection = useLpCategoriesSelection(); + + function getVisible(): number { + return fullData.reduce((count, item: LpCategory) => { + return item.visible === 'visible' ? count + 1 : count; + }, 0); + } + + function getHidden(): number { + return fullData.reduce((count, item: LpCategory) => { + return item.visible === 'hidden' ? count + 1 : count; + }, 0); + } + + // The tabs should be generated using API data. + const tabs = [ + { label: t('All'), value: '', count: totalCount }, + // { label: 'Active', value: 'active', count: 3 }, + // { label: 'Pending', value: 'pending', count: 1 }, + // { label: 'Blocked', value: 'blocked', count: 1 }, + { label: t('visible'), value: 'visible', count: visibleCount }, + { label: t('hidden'), value: 'hidden', count: hiddenCount }, + ] as const; + + const updateSearchParams = React.useCallback( + (newFilters: Filters, newSortDir: SortDir): void => { + const searchParams = new URLSearchParams(); + + if (newSortDir === 'asc') { + searchParams.set('sortDir', newSortDir); + } + + if (newFilters.status) { + searchParams.set('status', newFilters.status); + } + + if (newFilters.email) { + searchParams.set('email', newFilters.email); + } + + if (newFilters.phone) { + searchParams.set('phone', newFilters.phone); + } + + if (newFilters.name) { + searchParams.set('name', newFilters.name); + } + + if (newFilters.type) { + searchParams.set('type', newFilters.type); + } + + if (newFilters.visible) { + searchParams.set('visible', newFilters.visible); + } + + // NOTE: modify according to COLLECTION + router.push(`${paths.dashboard.lp_categories.list}?${searchParams.toString()}`); + }, + [router] + ); + + const handleClearFilters = React.useCallback(() => { + updateSearchParams({}, sortDir); + }, [updateSearchParams, sortDir]); + + const handleStatusChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, status: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleVisibleChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, visible: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleNameChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, name: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleTypeChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, type: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleEmailChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, email: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handlePhoneChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, phone: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleSortChange = React.useCallback( + (event: SelectChangeEvent) => { + updateSearchParams(filters, event.target.value as SortDir); + }, + [updateSearchParams, filters] + ); + + React.useEffect(() => { + const fetchCount = async (): Promise => { + try { + const tc = await GetAllCount(); + setTotalCount(tc); + + const vc = await GetVisibleCount(); + setVisibleCount(vc); + + const hc = await GetHiddenCount(); + setHiddenCount(hc); + } catch (error) { + // + } + }; + void fetchCount(); + }, []); + + const hasFilters = status || email || phone || visible || name || type; + + return ( +
+ + {tabs.map((tab) => ( + + } + iconPosition="end" + key={tab.value} + label={tab.label} + sx={{ minHeight: 'auto' }} + tabIndex={0} + value={tab.value} + /> + ))} + + + + + { + handleNameChange(value as string); + }} + onFilterDelete={() => { + handleNameChange(); + }} + popover={} + value={name} + /> + + { + handleTypeChange(value as string); + }} + onFilterDelete={() => { + handleTypeChange(); + }} + popover={} + value={type} + /> + + {hasFilters ? : null} + + {selection.selectedAny ? ( + + + {selection.selected.size} {t('selected')} + + + + ) : null} + + +
+ ); +} + +function TypeFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function NameFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function EmailFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function PhoneFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} +
+ + +'use client'; + +// lp-categories-pagination.tsx +// RULES: +// T.B.A. +import * as React from 'react'; +import TablePagination from '@mui/material/TablePagination'; + +function noop(): void { + return undefined; +} + +interface LpCategoriesPaginationProps { + count: number; + page: number; + // + setPage: (page: number) => void; + setRowsPerPage: (page: number) => void; + rowsPerPage: number; +} + +export function LpCategoriesPagination({ + count, + page, + // + setPage, + setRowsPerPage, + rowsPerPage, +}: LpCategoriesPaginationProps): React.JSX.Element { + // You should implement the pagination using a similar logic as the filters. + // Note that when page change, you should keep the filter search params. + const handleChangePage = (event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value)); + // console.log(parseInt(event.target.value)); + }; + + return ( + + ); +} + + + +'use client'; + +import * as React from 'react'; + +// import type { LessonCategory } from '@/types/lesson-type'; +import { useSelection } from '@/hooks/use-selection'; +import type { Selection } from '@/hooks/use-selection'; + +import { LpCategory } from './type'; + +function noop(): void { + return undefined; +} + +export interface LpCategoriesSelectionContextValue extends Selection {} + +export const LpCategoriesSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface LpCategoriesSelectionProviderProps { + children: React.ReactNode; + lessonCategories: LpCategory[]; +} + +export function LpCategoriesSelectionProvider({ + children, + lessonCategories = [], +}: LpCategoriesSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]); + const selection = useSelection(customerIds); + + return ( + {children} + ); +} + +export function useLpCategoriesSelection(): LpCategoriesSelectionContextValue { + return React.useContext(LpCategoriesSelectionContext); +} + + + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { LoadingButton } from '@mui/lab'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; +import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images'; +import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; + +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; + +import ConfirmDeleteModal from './confirm-delete-modal'; +import { useLpCategoriesSelection } from './lp-categories-selection-context'; +import type { LpCategory } from './type'; + +function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { + return [ + { + formatter: (row): React.JSX.Element => ( + + + + + + {' '} +
+ {row.cat_name} + + slug: {row.cat_name} + +
+
+ +
+ ), + name: 'Name', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} + + + ), + // NOTE: please refer to translation.json here + name: 'word-count', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => { + // eslint-disable-next-line react-hooks/rules-of-hooks + + const mapping = { + active: { + label: 'Active', + icon: ( + + ), + }, + blocked: { label: 'Blocked', icon: }, + pending: { + label: 'Pending', + icon: ( + + ), + }, + NA: { + label: 'NA', + icon: ( + + ), + }, + } as const; + const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; + + return ( + + ); + }, + name: 'Status', + width: '150px', + }, + { + formatter(row) { + return dayjs(row.createdAt).format('MMM D, YYYY'); + }, + name: 'Created at', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + + { + handleDeleteClick(row.id); + }} + > + + + + ), + name: 'Actions', + hideName: true, + width: '100px', + align: 'right', + }, + ]; +} + +export interface LessonCategoriesTableProps { + rows: LpCategory[]; + reloadRows: () => void; +} + +export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element { + const { t } = useTranslation(['lp_categories']); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpCategoriesSelection(); + + const [idToDelete, setIdToDelete] = React.useState(''); + const [open, setOpen] = React.useState(false); + + function handleDeleteClick(testId: string): void { + setOpen(true); + setIdToDelete(testId); + } + + return ( + + + + columns={columns(handleDeleteClick)} + onDeselectAll={deselectAll} + onDeselectOne={(_, row) => { + deselectOne(row.id); + }} + onSelectAll={selectAll} + onSelectOne={(_, row) => { + selectOne(row.id); + }} + rows={rows} + selectable + selected={selected} + /> + {!rows.length ? ( + + + {t('no-lesson-categories-found')} + + + ) : null} + + ); +} +
+ + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +import { Avatar, Divider, MenuItem, Select } from '@mui/material'; +// import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// import axios from 'axios'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import isDevelopment from '@/lib/check-is-development'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; + +import type { CreateFormProps } from './type'; + +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + cat_image: zod.array(zod.any()).optional(), + pos: zod.number().min(1, 'position is required').max(99), + init_answer: zod.string().optional(), + visible: zod.string(), + slug: zod.string().min(1, 'slug-is-required').max(255), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), + // TODO: remove me + type: zod.string().optional(), + isActive: zod.boolean().optional(), + order: zod.number().optional(), +}); + +type Values = zod.infer; + +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_QUIZ_LP_CATEGORIES).create(payload); + + logger.debug(result); + toast.success(t('create.success')); + router.push(paths.dashboard.lp_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('create.failed')); + } finally { + setIsCreating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(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)}
+
+
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +// +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +// +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +// +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; +import FormLoading from '@/components/loading'; + +import ErrorDisplay from '../../error'; +import type { EditFormProps } from './type'; + +// TODO: review this +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + // accept file object when user change image + // accept http string when user not changing image + cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(), + + // position + pos: zod.number().min(1, 'position is required').max(99), + + // it should be a valid JSON + init_answer: zod + .string() + .refine( + (value) => { + try { + JSON.parse(value); + return true; + } catch (error) { + return false; + } + }, + { message: 'init_answer must be a valid JSON' } + ) + .optional(), + visible: zod.string(), + slug: zod.string().min(0, 'slug-is-required').max(255).optional(), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), +}); + +type Values = zod.infer; + +const defaultValues = { + cat_name: '', + cat_image: undefined, + pos: 1, + init_answer: JSON.stringify({}), + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function LpCategoryEditForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['lp_categories']); + + const { cat_id: catId } = useParams<{ cat_id: string }>(); + // + const [isUpdating, setIsUpdating] = React.useState(false); + const [showLoading, setShowLoading] = React.useState(false); + // + const [showError, setShowError] = React.useState({ show: false, detail: '' }); + + const { + control, + handleSubmit, + formState: { errors }, + setValue, + reset, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + setIsUpdating(true); + + const tempUpdate: EditFormProps = { + cat_name: values.cat_name, + cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null, + pos: values.pos, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + init_answer: JSON.parse(values.init_answer || '{}'), + + visible: values.visible, + slug: values.slug || 'not-defined', + remarks: values.remarks, + description: values.description, + // + // TODO: remove below + type: '', + }; + + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate); + logger.debug(result); + toast.success(t('edit.success')); + router.push(paths.dashboard.lp_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('update.failed')); + } finally { + setIsUpdating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(null); + const avatar = watch('avatar'); + + const handleAvatarChange = React.useCallback( + async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + + if (file) { + const url = await fileToBase64(file); + setValue('avatar', url); + } + }, + [setValue] + ); + + // TODO: need to align with save form + // use trycatch + const [textDescription, setTextDescription] = React.useState(''); + const [textRemarks, setTextRemarks] = React.useState(''); + + // load existing data when user arrive + const loadExistingData = React.useCallback( + async (id: string) => { + setShowLoading(true); + + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); + + reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); + setTextDescription(result.description); + setTextRemarks(result.remarks); + + if (result.cat_image !== '') { + const fetchResult = await fetch( + `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}` + ); + + const blob = await fetchResult.blob(); + const url = await fileToBase64(blob); + + setValue('avatar', url); + } else { + setValue('avatar', ''); + } + } catch (error) { + logger.error(error); + toast(t('list.error')); + + setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); + } finally { + setShowLoading(false); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [catId] + ); + + React.useEffect(() => { + void loadExistingData(catId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [catId]); + + if (showLoading) return ; + if (showError.show) + return ( + + ); + + return ( +
+ + + } + spacing={4} + > + + {t('edit.basic-info')} + + + + + + + + + + {t('edit.avatar')} + {t('edit.avatarRequirements')} + + + + + + + ( + + {t('edit.cat_name')} + + {errors.cat_name ? {errors.cat_name.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.pos')} + { + field.onChange(parseInt(e.target.value)); + }} + type="number" + /> + {errors.pos ? {errors.pos.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.slug')} + + {errors.slug ? {errors.slug.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.visible')} + + + {errors.visible ? {errors.visible.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.init_answer')} + + {errors.init_answer ? {errors.init_answer.message} : null} + + )} + /> + + + + {/* */} + + {t('edit.detail-information')} + + + { + return ( + + + {t('edit.description')} + + + { + field.onChange({ target: { value: editor.getHTML() } }); + }} + placeholder={t('edit.description.default')} + /> + + + ); + }} + /> + + + ( + + + {t('edit.remarks')} + + + { + field.onChange({ target: { value: editor.getText() } }); + }} + hideToolbar + placeholder={t('edit.remarks.default')} + /> + + + )} + /> + + + + {/* */} + + + + + + + {t('edit.updateButton')} + + + +
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +// import { COL_LESSON_CATEGORIES } from '@/constants'; +import GetAllCount from '@/db/QuizListenings/GetAllCount'; +import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount'; +import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount'; +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import type { SelectChangeEvent } from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; +import { Option } from '@/components/core/option'; + +import { useLpCategoriesSelection } from './lp-categories-selection-context'; +import { LpCategory } from './type'; + +export interface Filters { + email?: string; + phone?: string; + status?: string; + name?: string; + visible?: string; + type?: string; +} + +export type SortDir = 'asc' | 'desc'; + +export interface LpCategoriesFiltersProps { + filters?: Filters; + sortDir?: SortDir; + fullData: LpCategory[]; +} + +export function LpCategoriesFilters({ + filters = {}, + sortDir = 'desc', + fullData, +}: LpCategoriesFiltersProps): React.JSX.Element { + const { t } = useTranslation(); + const { email, phone, status, name, visible, type } = filters; + + const [totalCount, setTotalCount] = React.useState(0); + const [visibleCount, setVisibleCount] = React.useState(0); + const [hiddenCount, setHiddenCount] = React.useState(0); + + const router = useRouter(); + + const selection = useLpCategoriesSelection(); + + function getVisible(): number { + return fullData.reduce((count, item: LpCategory) => { + return item.visible === 'visible' ? count + 1 : count; + }, 0); + } + + function getHidden(): number { + return fullData.reduce((count, item: LpCategory) => { + return item.visible === 'hidden' ? count + 1 : count; + }, 0); + } + + // The tabs should be generated using API data. + const tabs = [ + { label: t('All'), value: '', count: totalCount }, + // { label: 'Active', value: 'active', count: 3 }, + // { label: 'Pending', value: 'pending', count: 1 }, + // { label: 'Blocked', value: 'blocked', count: 1 }, + { label: t('visible'), value: 'visible', count: visibleCount }, + { label: t('hidden'), value: 'hidden', count: hiddenCount }, + ] as const; + + const updateSearchParams = React.useCallback( + (newFilters: Filters, newSortDir: SortDir): void => { + const searchParams = new URLSearchParams(); + + if (newSortDir === 'asc') { + searchParams.set('sortDir', newSortDir); + } + + if (newFilters.status) { + searchParams.set('status', newFilters.status); + } + + if (newFilters.email) { + searchParams.set('email', newFilters.email); + } + + if (newFilters.phone) { + searchParams.set('phone', newFilters.phone); + } + + if (newFilters.name) { + searchParams.set('name', newFilters.name); + } + + if (newFilters.type) { + searchParams.set('type', newFilters.type); + } + + if (newFilters.visible) { + searchParams.set('visible', newFilters.visible); + } + + // NOTE: modify according to COLLECTION + router.push(`${paths.dashboard.lp_categories.list}?${searchParams.toString()}`); + }, + [router] + ); + + const handleClearFilters = React.useCallback(() => { + updateSearchParams({}, sortDir); + }, [updateSearchParams, sortDir]); + + const handleStatusChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, status: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleVisibleChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, visible: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleNameChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, name: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleTypeChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, type: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleEmailChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, email: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handlePhoneChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, phone: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleSortChange = React.useCallback( + (event: SelectChangeEvent) => { + updateSearchParams(filters, event.target.value as SortDir); + }, + [updateSearchParams, filters] + ); + + React.useEffect(() => { + const fetchCount = async (): Promise => { + try { + const tc = await GetAllCount(); + setTotalCount(tc); + + const vc = await GetVisibleCount(); + setVisibleCount(vc); + + const hc = await GetHiddenCount(); + setHiddenCount(hc); + } catch (error) { + // + } + }; + void fetchCount(); + }, []); + + const hasFilters = status || email || phone || visible || name || type; + + return ( +
+ + {tabs.map((tab) => ( + + } + iconPosition="end" + key={tab.value} + label={tab.label} + sx={{ minHeight: 'auto' }} + tabIndex={0} + value={tab.value} + /> + ))} + + + + + { + handleNameChange(value as string); + }} + onFilterDelete={() => { + handleNameChange(); + }} + popover={} + value={name} + /> + + { + handleTypeChange(value as string); + }} + onFilterDelete={() => { + handleTypeChange(); + }} + popover={} + value={type} + /> + + {hasFilters ? : null} + + {selection.selectedAny ? ( + + + {selection.selected.size} {t('selected')} + + + + ) : null} + + +
+ ); +} + +function TypeFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function NameFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function EmailFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function PhoneFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} +
+ + +'use client'; + +import * as React from 'react'; +import TablePagination from '@mui/material/TablePagination'; + +function noop(): void { + return undefined; +} + +interface LessonCategoriesPaginationProps { + count: number; + page: number; + // + setPage: (page: number) => void; + setRowsPerPage: (page: number) => void; + rowsPerPage: number; +} + +export function LpCategoriesPagination({ + count, + page, + // + setPage, + setRowsPerPage, + rowsPerPage, +}: LessonCategoriesPaginationProps): React.JSX.Element { + // You should implement the pagination using a similar logic as the filters. + // Note that when page change, you should keep the filter search params. + const handleChangePage = (event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value)); + // console.log(parseInt(event.target.value)); + }; + + return ( + + ); +} + + + +'use client'; + +import * as React from 'react'; + +// import type { LessonCategory } from '@/types/lesson-type'; +import { useSelection } from '@/hooks/use-selection'; +import type { Selection } from '@/hooks/use-selection'; + +import { LpCategory } from './type'; + +function noop(): void { + return undefined; +} + +export interface LpCategoriesSelectionContextValue extends Selection {} + +export const LpCategoriesSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface LpCategoriesSelectionProviderProps { + children: React.ReactNode; + lessonCategories: LpCategory[]; +} + +export function LpCategoriesSelectionProvider({ + children, + lessonCategories = [], +}: LpCategoriesSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]); + const selection = useSelection(customerIds); + + return ( + {children} + ); +} + +export function useLpCategoriesSelection(): LpCategoriesSelectionContextValue { + return React.useContext(LpCategoriesSelectionContext); +} + + + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { LoadingButton } from '@mui/lab'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; +import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images'; +import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; + +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; + +import ConfirmDeleteModal from './confirm-delete-modal'; +import { useLpCategoriesSelection } from './lp-categories-selection-context'; +import type { LpCategory } from './type'; + +function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { + return [ + { + formatter: (row): React.JSX.Element => ( + + + + + + {' '} +
+ {row.cat_name} + + slug: {row.cat_name} + +
+
+ +
+ ), + name: 'Name', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} + + + ), + // NOTE: please refer to translation.json here + name: 'word-count', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => { + // eslint-disable-next-line react-hooks/rules-of-hooks + + const mapping = { + active: { + label: 'Active', + icon: ( + + ), + }, + blocked: { label: 'Blocked', icon: }, + pending: { + label: 'Pending', + icon: ( + + ), + }, + NA: { + label: 'NA', + icon: ( + + ), + }, + } as const; + const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; + + return ( + + ); + }, + name: 'Status', + width: '150px', + }, + { + formatter(row) { + return dayjs(row.createdAt).format('MMM D, YYYY'); + }, + name: 'Created at', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + + { + handleDeleteClick(row.id); + }} + > + + + + ), + name: 'Actions', + hideName: true, + width: '100px', + align: 'right', + }, + ]; +} + +export interface LessonCategoriesTableProps { + rows: LpCategory[]; + reloadRows: () => void; +} + +export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element { + const { t } = useTranslation(['lp_categories']); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpCategoriesSelection(); + + const [idToDelete, setIdToDelete] = React.useState(''); + const [open, setOpen] = React.useState(false); + + function handleDeleteClick(testId: string): void { + setOpen(true); + setIdToDelete(testId); + } + + return ( + + + + columns={columns(handleDeleteClick)} + onDeselectAll={deselectAll} + onDeselectOne={(_, row) => { + deselectOne(row.id); + }} + onSelectAll={selectAll} + onSelectOne={(_, row) => { + selectOne(row.id); + }} + rows={rows} + selectable + selected={selected} + /> + {!rows.length ? ( + + + {t('no-lesson-categories-found')} + + + ) : null} + + ); +} +
+ + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +import { Avatar, Divider, MenuItem, Select } from '@mui/material'; +// import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// import axios from 'axios'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import isDevelopment from '@/lib/check-is-development'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; + +import type { CreateFormProps } from './type'; + +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + cat_image: zod.array(zod.any()).optional(), + pos: zod.number().min(1, 'position is required').max(99), + init_answer: zod.string().optional(), + visible: zod.string(), + slug: zod.string().min(1, 'slug-is-required').max(255), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), + // TODO: remove me + type: zod.string().optional(), + isActive: zod.boolean().optional(), + order: zod.number().optional(), +}); + +type Values = zod.infer; + +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_QUIZ_LP_CATEGORIES).create(payload); + + logger.debug(result); + toast.success(t('create.success')); + router.push(paths.dashboard.lp_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('create.failed')); + } finally { + setIsCreating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(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)}
+
+
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +// +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +// +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +// +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; +import FormLoading from '@/components/loading'; + +import ErrorDisplay from '../../error'; +import type { EditFormProps } from './type'; + +// TODO: review this +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + // accept file object when user change image + // accept http string when user not changing image + cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(), + + // position + pos: zod.number().min(1, 'position is required').max(99), + + // it should be a valid JSON + init_answer: zod + .string() + .refine( + (value) => { + try { + JSON.parse(value); + return true; + } catch (error) { + return false; + } + }, + { message: 'init_answer must be a valid JSON' } + ) + .optional(), + visible: zod.string(), + slug: zod.string().min(0, 'slug-is-required').max(255).optional(), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), +}); + +type Values = zod.infer; + +const defaultValues = { + cat_name: '', + cat_image: undefined, + pos: 1, + init_answer: JSON.stringify({}), + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function LpCategoryEditForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['lp_categories']); + + const { cat_id: catId } = useParams<{ cat_id: string }>(); + // + const [isUpdating, setIsUpdating] = React.useState(false); + const [showLoading, setShowLoading] = React.useState(false); + // + const [showError, setShowError] = React.useState({ show: false, detail: '' }); + + const { + control, + handleSubmit, + formState: { errors }, + setValue, + reset, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + setIsUpdating(true); + + const tempUpdate: EditFormProps = { + cat_name: values.cat_name, + cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null, + pos: values.pos, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + init_answer: JSON.parse(values.init_answer || '{}'), + + visible: values.visible, + slug: values.slug || 'not-defined', + remarks: values.remarks, + description: values.description, + // + // TODO: remove below + type: '', + }; + + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate); + logger.debug(result); + toast.success(t('edit.success')); + router.push(paths.dashboard.lp_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('update.failed')); + } finally { + setIsUpdating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(null); + const avatar = watch('avatar'); + + const handleAvatarChange = React.useCallback( + async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + + if (file) { + const url = await fileToBase64(file); + setValue('avatar', url); + } + }, + [setValue] + ); + + // TODO: need to align with save form + // use trycatch + const [textDescription, setTextDescription] = React.useState(''); + const [textRemarks, setTextRemarks] = React.useState(''); + + // load existing data when user arrive + const loadExistingData = React.useCallback( + async (id: string) => { + setShowLoading(true); + + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); + + reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); + setTextDescription(result.description); + setTextRemarks(result.remarks); + + if (result.cat_image !== '') { + const fetchResult = await fetch( + `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}` + ); + + const blob = await fetchResult.blob(); + const url = await fileToBase64(blob); + + setValue('avatar', url); + } else { + setValue('avatar', ''); + } + } catch (error) { + logger.error(error); + toast(t('list.error')); + + setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); + } finally { + setShowLoading(false); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [catId] + ); + + React.useEffect(() => { + void loadExistingData(catId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [catId]); + + if (showLoading) return ; + if (showError.show) + return ( + + ); + + return ( +
+ + + } + spacing={4} + > + + {t('edit.basic-info')} + + + + + + + + + + {t('edit.avatar')} + {t('edit.avatarRequirements')} + + + + + + + ( + + {t('edit.cat_name')} + + {errors.cat_name ? {errors.cat_name.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.pos')} + { + field.onChange(parseInt(e.target.value)); + }} + type="number" + /> + {errors.pos ? {errors.pos.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.slug')} + + {errors.slug ? {errors.slug.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.visible')} + + + {errors.visible ? {errors.visible.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.init_answer')} + + {errors.init_answer ? {errors.init_answer.message} : null} + + )} + /> + + + + {/* */} + + {t('edit.detail-information')} + + + { + return ( + + + {t('edit.description')} + + + { + field.onChange({ target: { value: editor.getHTML() } }); + }} + placeholder={t('edit.description.default')} + /> + + + ); + }} + /> + + + ( + + + {t('edit.remarks')} + + + { + field.onChange({ target: { value: editor.getText() } }); + }} + hideToolbar + placeholder={t('edit.remarks.default')} + /> + + + )} + /> + + + + {/* */} + + + + + + + {t('edit.updateButton')} + + + +
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; + +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; +import { Option } from '@/components/core/option'; + +export interface Notification { + id: string; + type: string; + status: 'delivered' | 'pending' | 'failed'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {row.type} + + ), + name: 'Type', + width: '300px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + delivered: { label: 'Delivered', color: 'success' }, + pending: { label: 'Pending', color: 'warning' }, + failed: { label: 'Failed', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface NotificationsProps { + notifications: Notification[]; +} + +export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { + return ( + + + + + } + title="Notifications" + /> + + + + +
+ +
+
+ + + columns={columns} rows={notifications} /> + + +
+
+
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple'; + +import { dayjs } from '@/lib/dayjs'; +import type { ColumnDef } from '@/components/core/data-table'; +import { DataTable } from '@/components/core/data-table'; + +export interface Payment { + currency: string; + amount: number; + invoiceId: string; + status: 'pending' | 'completed' | 'canceled' | 'refunded'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)} + + ), + name: 'Amount', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + pending: { label: 'Pending', color: 'warning' }, + completed: { label: 'Completed', color: 'success' }, + canceled: { label: 'Canceled', color: 'error' }, + refunded: { label: 'Refunded', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + return {row.invoiceId}; + }, + name: 'Invoice ID', + width: '150px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface PaymentsProps { + ordersValue: number; + payments: Payment[]; + refundsValue: number; + totalOrders: number; +} + +export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { + return ( + + }> + Create Payment + + } + avatar={ + + + + } + title="Payments" + /> + + + + } + spacing={3} + sx={{ justifyContent: 'space-between', p: 2 }} + > +
+ + Total orders + + {new Intl.NumberFormat('en-US').format(totalOrders)} +
+
+ + Orders value + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} + +
+
+ + Refunds + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} + +
+
+
+ + + columns={columns} rows={payments} /> + + +
+
+
+ ); +} +
+ + +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; + +export interface Address { + id: string; + country: string; + state: string; + city: string; + zipCode: string; + street: string; + primary?: boolean; +} + +export interface ShippingAddressProps { + address: Address; +} + +export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement { + return ( + + + + + {address.street}, +
+ {address.city}, {address.state}, {address.country}, +
+ {address.zipCode} +
+ + {address.primary ? : } + + +
+
+
+ ); +} +
+ + +export interface LpCategory { + isEmpty?: boolean; + // + id: string; + collectionId: string; + // + cat_name: string; + cat_image_url?: string; + cat_image?: string; + pos: number; + visible: string; + lesson_id: string; + description: string; + remarks: string; + slug: string; + init_answer: any; + // + name: string; + avatar: string; + email: string; + phone: string; + quota: number; + status: 'pending' | 'active' | 'blocked' | 'NA'; + createdAt: Date; +} + +export interface CreateFormProps { + cat_name: string; + cat_image: File[] | null; + pos: number; + init_answer?: string; + visible: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: to remove + type: string; + isActive: boolean; + order: number; + name?: string; + imageUrl?: string; +} + +export interface EditFormProps { + cat_name: string; + cat_image: File[] | null; + pos: number; + init_answer: any; + visible: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: remove below + type: string; +} + +export interface Helloworld { + helloworld: string; +} + + + +import { dayjs } from '@/lib/dayjs'; + +import { CreateFormProps, LpQuestion } from './type'; + +export const defaultLpQuestion: LpQuestion = { + isEmpty: false, + id: 'default-id', + cat_name: 'default-question-name', + cat_image_url: undefined, + cat_image: undefined, + pos: 0, + visible: 'hidden', + lesson_id: 'default-lesson-id', + description: 'default-description', + remarks: 'default-remarks', + slug: '', + init_answer: {}, + // from pocketbase + collectionId: '0000000000', + createdAt: dayjs('2099-01-01').toDate(), + // + name: '', + avatar: '', + email: '', + phone: '', + quota: 0, + status: 'NA', +}; + +// export const LpCategoryCreateFormDefault: CreateFormProps = { +// name: '', +// type: '', +// pos: 1, +// visible: 'visible', +// description: '', +// isActive: true, +// order: 1, +// imageUrl: '', +// }; + +export const emptyLpQuestion: LpQuestion = { + ...defaultLpQuestion, + isEmpty: true, +}; + + + +please review and add translations, e.g. `{t('[word]')}` + +--- + +please help to review the `tsx` file in this folder +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/questions` + +it was clone from +`category`/`categories`, `lp_category`/`lp_categories` +please help to modify to `question`/`questions`, `lp_question`/`lp_questions` + +please also help to modify the name of +`variables`, `constants`, `functions`, `classes`, components's name, paths + +the db fields structures are the same + +do not move the files +do not create directories +keep current folder structure is important + +thanks + + + +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_TYPES } from '@/constants'; +import deleteQuizLPCategories from '@/db/QuizListenings/Delete'; +import { LoadingButton } from '@mui/lab'; +import { Button, Container, Modal, Paper } from '@mui/material'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note'; +import PocketBase from 'pocketbase'; +import { useTranslation } from 'react-i18next'; + +import { logger } from '@/lib/default-logger'; +import { toast } from '@/components/core/toaster'; + +export default function ConfirmDeleteModal({ + open, + setOpen, + idToDelete, + reloadRows, +}: { + open: boolean; + setOpen: (b: boolean) => void; + idToDelete: string; + reloadRows: () => void; +}): React.JSX.Element { + const { t } = useTranslation(); + + // const handleClose = () => setOpen(false); + function handleClose(): void { + setOpen(false); + } + + const [isDeleteing, setIsDeleteing] = React.useState(false); + const style = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + }; + + function handleUserConfirmDelete(): void { + if (idToDelete) { + setIsDeleteing(true); + deleteQuizLPCategories(idToDelete) + .then(() => { + reloadRows(); + handleClose(); + toast(t('delete.success')); + }) + .catch((err) => { + // console.error(err) + logger.error(err); + toast(t('delete.error')); + }) + .finally(() => { + setIsDeleteing(false); + }); + } + } + + return ( +
+ + + + + + + + + + + {t('Delete Lesson Type ?')} + + {t('Are you sure you want to delete lesson type ?')} + + + + + { + handleUserConfirmDelete(); + }} + loading={isDeleteing} + > + {t('Delete')} + + + + + + + + +
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +import { Avatar, Divider, MenuItem, Select } from '@mui/material'; +// import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// import axios from 'axios'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import isDevelopment from '@/lib/check-is-development'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; + +import type { CreateFormProps } from './type'; + +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + cat_image: zod.array(zod.any()).optional(), + pos: zod.number().min(1, 'position is required').max(99), + init_answer: zod.string().optional(), + visible: zod.string(), + slug: zod.string().min(1, 'slug-is-required').max(255), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), + // TODO: remove me + type: zod.string().optional(), + isActive: zod.boolean().optional(), + order: zod.number().optional(), +}); + +type Values = zod.infer; + +export const defaultValues = { + cat_name: '', + cat_image: undefined, + pos: 1, + init_answer: '', + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function LpQuestionCreateForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['lp_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_QUIZ_LP_CATEGORIES).create(payload); + + logger.debug(result); + toast.success(t('create.success')); + router.push(paths.dashboard.lp_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('create.failed')); + } finally { + setIsCreating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(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)}
+
+
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +// +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +// +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +// +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; +import FormLoading from '@/components/loading'; + +import ErrorDisplay from '../../error'; +import type { EditFormProps } from './type'; + +// TODO: review this +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + // accept file object when user change image + // accept http string when user not changing image + cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(), + + // position + pos: zod.number().min(1, 'position is required').max(99), + + // it should be a valid JSON + init_answer: zod + .string() + .refine( + (value) => { + try { + JSON.parse(value); + return true; + } catch (error) { + return false; + } + }, + { message: 'init_answer must be a valid JSON' } + ) + .optional(), + visible: zod.string(), + slug: zod.string().min(0, 'slug-is-required').max(255).optional(), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), +}); + +type Values = zod.infer; + +const defaultValues = { + cat_name: '', + cat_image: undefined, + pos: 1, + init_answer: JSON.stringify({}), + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function LpQuestionEditForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['lp_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, + handleSubmit, + formState: { errors }, + setValue, + reset, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + setIsUpdating(true); + + const tempUpdate: EditFormProps = { + cat_name: values.cat_name, + cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null, + pos: values.pos, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + init_answer: JSON.parse(values.init_answer || '{}'), + + visible: values.visible, + slug: values.slug || 'not-defined', + remarks: values.remarks, + description: values.description, + // + // TODO: remove below + type: '', + }; + + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate); + logger.debug(result); + toast.success(t('edit.success')); + router.push(paths.dashboard.lp_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('update.failed')); + } finally { + setIsUpdating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(null); + const avatar = watch('avatar'); + + const handleAvatarChange = React.useCallback( + async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + + if (file) { + const url = await fileToBase64(file); + setValue('avatar', url); + } + }, + [setValue] + ); + + // TODO: need to align with save form + // use trycatch + const [textDescription, setTextDescription] = React.useState(''); + const [textRemarks, setTextRemarks] = React.useState(''); + + // load existing data when user arrive + const loadExistingData = React.useCallback( + async (id: string) => { + setShowLoading(true); + + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); + + reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); + setTextDescription(result.description); + setTextRemarks(result.remarks); + + if (result.cat_image !== '') { + const fetchResult = await fetch( + `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}` + ); + + const blob = await fetchResult.blob(); + const url = await fileToBase64(blob); + + setValue('avatar', url); + } else { + setValue('avatar', ''); + } + } catch (error) { + logger.error(error); + toast(t('list.error')); + + setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); + } finally { + setShowLoading(false); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [catId] + ); + + React.useEffect(() => { + void loadExistingData(catId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [catId]); + + if (showLoading) return ; + if (showError.show) + return ( + + ); + + return ( +
+ + + } + spacing={4} + > + + {t('edit.basic-info')} + + + + + + + + + + {t('edit.avatar')} + {t('edit.avatarRequirements')} + + + + + + + ( + + {t('edit.cat_name')} + + {errors.cat_name ? {errors.cat_name.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.pos')} + { + field.onChange(parseInt(e.target.value)); + }} + type="number" + /> + {errors.pos ? {errors.pos.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.slug')} + + {errors.slug ? {errors.slug.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.visible')} + + + {errors.visible ? {errors.visible.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.init_answer')} + + {errors.init_answer ? {errors.init_answer.message} : null} + + )} + /> + + + + {/* */} + + {t('edit.detail-information')} + + + { + return ( + + + {t('edit.description')} + + + { + field.onChange({ target: { value: editor.getHTML() } }); + }} + placeholder={t('edit.description.default')} + /> + + + ); + }} + /> + + + ( + + + {t('edit.remarks')} + + + { + field.onChange({ target: { value: editor.getText() } }); + }} + hideToolbar + placeholder={t('edit.remarks.default')} + /> + + + )} + /> + + + + {/* */} + + + + + + + {t('edit.updateButton')} + + + +
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +// import { COL_LESSON_CATEGORIES } from '@/constants'; +import GetAllCount from '@/db/QuizListenings/GetAllCount'; +import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount'; +import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount'; +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import type { SelectChangeEvent } from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; +import { Option } from '@/components/core/option'; + +import { useLpQuestionsSelection } from './lp-questions-selection-context'; +import { LpQuestion } from './type'; + +export interface Filters { + email?: string; + phone?: string; + status?: string; + name?: string; + visible?: string; + type?: string; +} + +export type SortDir = 'asc' | 'desc'; + +export interface LpQuestionsFiltersProps { + filters?: Filters; + sortDir?: SortDir; + fullData: LpQuestion[]; +} + +export function LpQuestionsFilters({ + filters = {}, + sortDir = 'desc', + fullData, +}: LpQuestionsFiltersProps): React.JSX.Element { + const { t } = useTranslation(); + const { email, phone, status, name, visible, type } = filters; + + const [totalCount, setTotalCount] = React.useState(0); + const [visibleCount, setVisibleCount] = React.useState(0); + const [hiddenCount, setHiddenCount] = React.useState(0); + + const router = useRouter(); + + const selection = useLpQuestionsSelection(); + + function getVisible(): number { + return fullData.reduce((count, item: LpQuestion) => { + return item.visible === 'visible' ? count + 1 : count; + }, 0); + } + + function getHidden(): number { + return fullData.reduce((count, item: LpQuestion) => { + return item.visible === 'hidden' ? count + 1 : count; + }, 0); + } + + // The tabs should be generated using API data. + const tabs = [ + { label: t('All'), value: '', count: totalCount }, + // { label: 'Active', value: 'active', count: 3 }, + // { label: 'Pending', value: 'pending', count: 1 }, + // { label: 'Blocked', value: 'blocked', count: 1 }, + { label: t('visible'), value: 'visible', count: visibleCount }, + { label: t('hidden'), value: 'hidden', count: hiddenCount }, + ] as const; + + const updateSearchParams = React.useCallback( + (newFilters: Filters, newSortDir: SortDir): void => { + const searchParams = new URLSearchParams(); + + if (newSortDir === 'asc') { + searchParams.set('sortDir', newSortDir); + } + + if (newFilters.status) { + searchParams.set('status', newFilters.status); + } + + if (newFilters.email) { + searchParams.set('email', newFilters.email); + } + + if (newFilters.phone) { + searchParams.set('phone', newFilters.phone); + } + + if (newFilters.name) { + searchParams.set('name', newFilters.name); + } + + if (newFilters.type) { + searchParams.set('type', newFilters.type); + } + + if (newFilters.visible) { + searchParams.set('visible', newFilters.visible); + } + + // NOTE: modify according to COLLECTION + router.push(`${paths.dashboard.lp_questions.list}?${searchParams.toString()}`); + }, + [router] + ); + + const handleClearFilters = React.useCallback(() => { + updateSearchParams({}, sortDir); + }, [updateSearchParams, sortDir]); + + const handleStatusChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, status: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleVisibleChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, visible: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleNameChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, name: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleTypeChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, type: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleEmailChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, email: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handlePhoneChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, phone: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleSortChange = React.useCallback( + (event: SelectChangeEvent) => { + updateSearchParams(filters, event.target.value as SortDir); + }, + [updateSearchParams, filters] + ); + + React.useEffect(() => { + const fetchCount = async (): Promise => { + try { + const tc = await GetAllCount(); + setTotalCount(tc); + + const vc = await GetVisibleCount(); + setVisibleCount(vc); + + const hc = await GetHiddenCount(); + setHiddenCount(hc); + } catch (error) { + // + } + }; + void fetchCount(); + }, []); + + const hasFilters = status || email || phone || visible || name || type; + + return ( +
+ + {tabs.map((tab) => ( + + } + iconPosition="end" + key={tab.value} + label={tab.label} + sx={{ minHeight: 'auto' }} + tabIndex={0} + value={tab.value} + /> + ))} + + + + + { + handleNameChange(value as string); + }} + onFilterDelete={() => { + handleNameChange(); + }} + popover={} + value={name} + /> + + { + handleTypeChange(value as string); + }} + onFilterDelete={() => { + handleTypeChange(); + }} + popover={} + value={type} + /> + + {hasFilters ? : null} + + {selection.selectedAny ? ( + + + {selection.selected.size} {t('selected')} + + + + ) : null} + + +
+ ); +} + +function TypeFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function NameFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function EmailFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function PhoneFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} +
+ + +'use client'; + +import * as React from 'react'; +import TablePagination from '@mui/material/TablePagination'; + +function noop(): void { + return undefined; +} + +interface LessonQuestionsPaginationProps { + count: number; + page: number; + // + setPage: (page: number) => void; + setRowsPerPage: (page: number) => void; + rowsPerPage: number; +} + +export function LpQuestionsPagination({ + count, + page, + // + setPage, + setRowsPerPage, + rowsPerPage, +}: LessonQuestionsPaginationProps): React.JSX.Element { + // You should implement the pagination using a similar logic as the filters. + // Note that when page change, you should keep the filter search params. + const handleChangePage = (event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value)); + // console.log(parseInt(event.target.value)); + }; + + return ( + + ); +} + + + +'use client'; + +import * as React from 'react'; + +// import type { LessonCategory } from '@/types/lesson-type'; +import { useSelection } from '@/hooks/use-selection'; +import type { Selection } from '@/hooks/use-selection'; + +import type { LpQuestion } from './type'; + +function noop(): void { + return undefined; +} + +export interface LpQuestionsSelectionContextValue extends Selection {} + +export const LpQuestionsSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface LpQuestionsSelectionProviderProps { + children: React.ReactNode; + lessonQuestions: LpQuestion[]; +} + +export function LpQuestionsSelectionProvider({ + children, + lessonQuestions = [], +}: LpQuestionsSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => lessonQuestions.map((customer) => customer.id), [lessonQuestions]); + const selection = useSelection(customerIds); + + return ( + {children} + ); +} + +export function useLpQuestionsSelection(): LpQuestionsSelectionContextValue { + return React.useContext(LpQuestionsSelectionContext); +} + + + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { LoadingButton } from '@mui/lab'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; +import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images'; +import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; + +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; + +import ConfirmDeleteModal from './confirm-delete-modal'; +import { useLpQuestionsSelection } from './lp-questions-selection-context'; +import type { LpQuestion } from './type'; + +function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { + return [ + { + formatter: (row): React.JSX.Element => ( + + + + + + {' '} +
+ {row.cat_name} + + slug: {row.cat_name} + +
+
+ +
+ ), + name: 'Name', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} + + + ), + // NOTE: please refer to translation.json here + name: 'word-count', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => { + // eslint-disable-next-line react-hooks/rules-of-hooks + + const mapping = { + active: { + label: 'Active', + icon: ( + + ), + }, + blocked: { label: 'Blocked', icon: }, + pending: { + label: 'Pending', + icon: ( + + ), + }, + NA: { + label: 'NA', + icon: ( + + ), + }, + } as const; + const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; + + return ( + + ); + }, + name: 'Status', + width: '150px', + }, + { + formatter(row) { + return dayjs(row.createdAt).format('MMM D, YYYY'); + }, + name: 'Created at', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + + { + handleDeleteClick(row.id); + }} + > + + + + ), + name: 'Actions', + hideName: true, + width: '100px', + align: 'right', + }, + ]; +} + +export interface LessonQuestionsTableProps { + rows: LpQuestion[]; + reloadRows: () => void; +} + +export function LpQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps): React.JSX.Element { + const { t } = useTranslation(['lp_categories']); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpQuestionsSelection(); + + const [idToDelete, setIdToDelete] = React.useState(''); + const [open, setOpen] = React.useState(false); + + function handleDeleteClick(testId: string): void { + setOpen(true); + setIdToDelete(testId); + } + + return ( + + + + columns={columns(handleDeleteClick)} + onDeselectAll={deselectAll} + onDeselectOne={(_, row) => { + deselectOne(row.id); + }} + onSelectAll={selectAll} + onSelectOne={(_, row) => { + selectOne(row.id); + }} + rows={rows} + selectable + selected={selected} + /> + {!rows.length ? ( + + + {/* TODO: update this */} + {t('no-lesson-categories-found')} + + + ) : null} + + ); +} +
+ + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; + +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; +import { Option } from '@/components/core/option'; + +export interface Notification { + id: string; + type: string; + status: 'delivered' | 'pending' | 'failed'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {row.type} + + ), + name: 'Type', + width: '300px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + delivered: { label: 'Delivered', color: 'success' }, + pending: { label: 'Pending', color: 'warning' }, + failed: { label: 'Failed', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface NotificationsProps { + notifications: Notification[]; +} + +export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { + return ( + + + + + } + title="Notifications" + /> + + + + +
+ +
+
+ + + columns={columns} rows={notifications} /> + + +
+
+
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple'; + +import { dayjs } from '@/lib/dayjs'; +import type { ColumnDef } from '@/components/core/data-table'; +import { DataTable } from '@/components/core/data-table'; + +export interface Payment { + currency: string; + amount: number; + invoiceId: string; + status: 'pending' | 'completed' | 'canceled' | 'refunded'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)} + + ), + name: 'Amount', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + pending: { label: 'Pending', color: 'warning' }, + completed: { label: 'Completed', color: 'success' }, + canceled: { label: 'Canceled', color: 'error' }, + refunded: { label: 'Refunded', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + return {row.invoiceId}; + }, + name: 'Invoice ID', + width: '150px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface PaymentsProps { + ordersValue: number; + payments: Payment[]; + refundsValue: number; + totalOrders: number; +} + +export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { + return ( + + }> + Create Payment + + } + avatar={ + + + + } + title="Payments" + /> + + + + } + spacing={3} + sx={{ justifyContent: 'space-between', p: 2 }} + > +
+ + Total orders + + {new Intl.NumberFormat('en-US').format(totalOrders)} +
+
+ + Orders value + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} + +
+
+ + Refunds + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} + +
+
+
+ + + columns={columns} rows={payments} /> + + +
+
+
+ ); +} +
+ + +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; + +export interface Address { + id: string; + country: string; + state: string; + city: string; + zipCode: string; + street: string; + primary?: boolean; +} + +export interface ShippingAddressProps { + address: Address; +} + +export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement { + return ( + + + + + {address.street}, +
+ {address.city}, {address.state}, {address.country}, +
+ {address.zipCode} +
+ + {address.primary ? : } + + +
+
+
+ ); +} +
+ + +export interface LpQuestion { + isEmpty?: boolean; + // + id: string; + collectionId: string; + // + cat_name: string; + cat_image_url?: string; + cat_image?: string; + pos: number; + visible: string; + lesson_id: string; + description: string; + remarks: string; + slug: string; + init_answer: any; + // + name: string; + avatar: string; + email: string; + phone: string; + quota: number; + status: 'pending' | 'active' | 'blocked' | 'NA'; + createdAt: Date; +} + +export interface CreateFormProps { + cat_name: string; + cat_image: File[] | null; + pos: number; + init_answer?: string; + visible: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: to remove + type: string; + isActive: boolean; + order: number; + name?: string; + imageUrl?: string; +} + +export interface EditFormProps { + cat_name: string; + cat_image: File[] | null; + pos: number; + init_answer: any; + visible: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: remove below + type: string; +} + +export interface Helloworld { + helloworld: string; +} + + +
diff --git a/002_source/cms/src/components/dashboard/mf/categories/mf-categories-table.tsx b/002_source/cms/src/components/dashboard/mf/categories/mf-categories-table.tsx index 612efdd..3cc015c 100644 --- a/002_source/cms/src/components/dashboard/mf/categories/mf-categories-table.tsx +++ b/002_source/cms/src/components/dashboard/mf/categories/mf-categories-table.tsx @@ -232,6 +232,7 @@ export function MfCategoriesTable({ rows, reloadRows }: LessonCategoriesTablePro sx={{ textAlign: 'center' }} variant="body2" > + {/* TODO: update this */} {t('no-lesson-categories-found')} diff --git a/002_source/cms/src/components/dashboard/mf/questions/mf-questions-table.tsx b/002_source/cms/src/components/dashboard/mf/questions/mf-questions-table.tsx index c65a296..7202f0a 100644 --- a/002_source/cms/src/components/dashboard/mf/questions/mf-questions-table.tsx +++ b/002_source/cms/src/components/dashboard/mf/questions/mf-questions-table.tsx @@ -232,6 +232,7 @@ export function MfQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps sx={{ textAlign: 'center' }} variant="body2" > + {/* TODO: update this */} {t('no-lesson-categories-found')} diff --git a/002_source/cms/src/components/dashboard/mf/repomix-output.xml b/002_source/cms/src/components/dashboard/mf/repomix-output.xml new file mode 100644 index 0000000..6edb6b6 --- /dev/null +++ b/002_source/cms/src/components/dashboard/mf/repomix-output.xml @@ -0,0 +1,4633 @@ +This file is a merged representation of the entire codebase, combined into a single document by Repomix. + + +This section contains a summary of this file. + + +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + + + +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Repository files, each consisting of: + - File path as an attribute + - Full contents of the file + + + +- This file should be treated as read-only. Any changes should be made to the + original repository files, not this packed version. +- When processing this file, use the file path to distinguish + between different files in the repository. +- Be aware that this file may contain sensitive information. Handle it with + the same level of security as you would the original repository. + + + +- Some files may have been excluded based on .gitignore rules and Repomix's configuration +- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files +- Files matching patterns in .gitignore are excluded +- Files matching default ignore patterns are excluded +- Files are sorted by Git change count (files with more changes are at the bottom) + + + + + + + + + +categories/ + _constants.ts + _PROMPT.MD + confirm-delete-modal.tsx + mf-categories-filters.tsx + mf-categories-pagination.tsx + mf-categories-selection-context.tsx + mf-categories-table.tsx + mf-category-create-form.tsx + mf-category-edit-form.tsx + notifications.tsx + payments.tsx + shipping-address.tsx + type.d.ts +questions/ + _constants.ts + _PROMPT.MD + confirm-delete-modal.tsx + mf-question-create-form.tsx + mf-question-edit-form.tsx + mf-questions-filters.tsx + mf-questions-pagination.tsx + mf-questions-selection-context.tsx + mf-questions-table.tsx + notifications.tsx + payments.tsx + shipping-address.tsx + type.d.ts + + + +This section contains the contents of the repository's files. + + +import { dayjs } from '@/lib/dayjs'; + +import { CreateFormProps, MfCategory } from './type'; + +export const defaultMfCategory: MfCategory = { + isEmpty: false, + id: 'default-id', + cat_name: 'default-category-name', + cat_image_url: undefined, + cat_image: undefined, + pos: 0, + visible: 'hidden', + lesson_id: 'default-lesson-id', + description: 'default-description', + remarks: 'default-remarks', + slug: '', + init_answer: {}, + // from pocketbase + collectionId: '0000000000', + createdAt: dayjs('2099-01-01').toDate(), + // + name: '', + avatar: '', + email: '', + phone: '', + quota: 0, + status: 'NA', +}; + +// export const LpCategoryCreateFormDefault: CreateFormProps = { +// name: '', +// type: '', +// pos: 1, +// visible: 'visible', +// description: '', +// isActive: true, +// order: 1, +// imageUrl: '', +// }; + +export const emptyLpCategory: MfCategory = { + ...defaultMfCategory, + isEmpty: true, +}; + + + +please review and add translations, e.g. `{t('[word]')}` + + + +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_TYPES } from '@/constants'; +import deleteQuizLPCategories from '@/db/QuizListenings/Delete'; +import { LoadingButton } from '@mui/lab'; +import { Button, Container, Modal, Paper } from '@mui/material'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note'; +import PocketBase from 'pocketbase'; +import { useTranslation } from 'react-i18next'; + +import { logger } from '@/lib/default-logger'; +import { toast } from '@/components/core/toaster'; + +export default function ConfirmDeleteModal({ + open, + setOpen, + idToDelete, + reloadRows, +}: { + open: boolean; + setOpen: (b: boolean) => void; + idToDelete: string; + reloadRows: () => void; +}): React.JSX.Element { + const { t } = useTranslation(); + + // const handleClose = () => setOpen(false); + function handleClose(): void { + setOpen(false); + } + + const [isDeleteing, setIsDeleteing] = React.useState(false); + const style = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + }; + + function handleUserConfirmDelete(): void { + if (idToDelete) { + setIsDeleteing(true); + deleteQuizLPCategories(idToDelete) + .then(() => { + reloadRows(); + handleClose(); + toast(t('delete.success')); + }) + .catch((err) => { + // console.error(err) + logger.error(err); + toast(t('delete.error')); + }) + .finally(() => { + setIsDeleteing(false); + }); + } + } + + return ( +
+ + + + + + + + + + + {t('Delete Lesson Type ?')} + + {t('Are you sure you want to delete lesson type ?')} + + + + + { + handleUserConfirmDelete(); + }} + loading={isDeleteing} + > + {t('Delete')} + + + + + + + + +
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +// import { COL_LESSON_CATEGORIES } from '@/constants'; +import GetAllCount from '@/db/QuizListenings/GetAllCount'; +import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount'; +import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount'; +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import type { SelectChangeEvent } from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; +import { Option } from '@/components/core/option'; + +import { useMfCategoriesSelection } from './mf-categories-selection-context'; +import { MfCategory } from './type'; + +export interface Filters { + email?: string; + phone?: string; + status?: string; + name?: string; + visible?: string; + type?: string; +} + +export type SortDir = 'asc' | 'desc'; + +export interface MfCategoriesFiltersProps { + filters?: Filters; + sortDir?: SortDir; + fullData: MfCategory[]; +} + +export function MfCategoriesFilters({ + filters = {}, + sortDir = 'desc', + fullData, +}: MfCategoriesFiltersProps): React.JSX.Element { + const { t } = useTranslation(); + const { email, phone, status, name, visible, type } = filters; + + const [totalCount, setTotalCount] = React.useState(0); + const [visibleCount, setVisibleCount] = React.useState(0); + const [hiddenCount, setHiddenCount] = React.useState(0); + + const router = useRouter(); + + const selection = useMfCategoriesSelection(); + + function getVisible(): number { + return fullData.reduce((count, item: MfCategory) => { + return item.visible === 'visible' ? count + 1 : count; + }, 0); + } + + function getHidden(): number { + return fullData.reduce((count, item: MfCategory) => { + return item.visible === 'hidden' ? count + 1 : count; + }, 0); + } + + // The tabs should be generated using API data. + const tabs = [ + { label: t('All'), value: '', count: totalCount }, + // { label: 'Active', value: 'active', count: 3 }, + // { label: 'Pending', value: 'pending', count: 1 }, + // { label: 'Blocked', value: 'blocked', count: 1 }, + { label: t('visible'), value: 'visible', count: visibleCount }, + { label: t('hidden'), value: 'hidden', count: hiddenCount }, + ] as const; + + const updateSearchParams = React.useCallback( + (newFilters: Filters, newSortDir: SortDir): void => { + const searchParams = new URLSearchParams(); + + if (newSortDir === 'asc') { + searchParams.set('sortDir', newSortDir); + } + + if (newFilters.status) { + searchParams.set('status', newFilters.status); + } + + if (newFilters.email) { + searchParams.set('email', newFilters.email); + } + + if (newFilters.phone) { + searchParams.set('phone', newFilters.phone); + } + + if (newFilters.name) { + searchParams.set('name', newFilters.name); + } + + if (newFilters.type) { + searchParams.set('type', newFilters.type); + } + + if (newFilters.visible) { + searchParams.set('visible', newFilters.visible); + } + + // NOTE: modify according to COLLECTION + router.push(`${paths.dashboard.mf_categories.list}?${searchParams.toString()}`); + }, + [router] + ); + + const handleClearFilters = React.useCallback(() => { + updateSearchParams({}, sortDir); + }, [updateSearchParams, sortDir]); + + const handleStatusChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, status: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleVisibleChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, visible: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleNameChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, name: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleTypeChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, type: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleEmailChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, email: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handlePhoneChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, phone: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleSortChange = React.useCallback( + (event: SelectChangeEvent) => { + updateSearchParams(filters, event.target.value as SortDir); + }, + [updateSearchParams, filters] + ); + + React.useEffect(() => { + const fetchCount = async (): Promise => { + try { + const tc = await GetAllCount(); + setTotalCount(tc); + + const vc = await GetVisibleCount(); + setVisibleCount(vc); + + const hc = await GetHiddenCount(); + setHiddenCount(hc); + } catch (error) { + // + } + }; + void fetchCount(); + }, []); + + const hasFilters = status || email || phone || visible || name || type; + + return ( +
+ + {tabs.map((tab) => ( + + } + iconPosition="end" + key={tab.value} + label={tab.label} + sx={{ minHeight: 'auto' }} + tabIndex={0} + value={tab.value} + /> + ))} + + + + + { + handleNameChange(value as string); + }} + onFilterDelete={() => { + handleNameChange(); + }} + popover={} + value={name} + /> + + { + handleTypeChange(value as string); + }} + onFilterDelete={() => { + handleTypeChange(); + }} + popover={} + value={type} + /> + + {hasFilters ? : null} + + {selection.selectedAny ? ( + + + {selection.selected.size} {t('selected')} + + + + ) : null} + + +
+ ); +} + +function TypeFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function NameFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function EmailFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function PhoneFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} +
+ + +'use client'; + +import * as React from 'react'; +import TablePagination from '@mui/material/TablePagination'; + +function noop(): void { + return undefined; +} + +interface LessonCategoriesPaginationProps { + count: number; + page: number; + // + setPage: (page: number) => void; + setRowsPerPage: (page: number) => void; + rowsPerPage: number; +} + +export function MfCategoriesPagination({ + count, + page, + // + setPage, + setRowsPerPage, + rowsPerPage, +}: LessonCategoriesPaginationProps): React.JSX.Element { + // You should implement the pagination using a similar logic as the filters. + // Note that when page change, you should keep the filter search params. + const handleChangePage = (event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value)); + // console.log(parseInt(event.target.value)); + }; + + return ( + + ); +} + + + +'use client'; + +import * as React from 'react'; + +// import type { LessonCategory } from '@/types/lesson-type'; +import { useSelection } from '@/hooks/use-selection'; +import type { Selection } from '@/hooks/use-selection'; + +import { MfCategory } from './type'; + +function noop(): void { + return undefined; +} + +export interface MfCategoriesSelectionContextValue extends Selection {} + +export const MfCategoriesSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface MfCategoriesSelectionProviderProps { + children: React.ReactNode; + lessonCategories: MfCategory[]; +} + +export function MfCategoriesSelectionProvider({ + children, + lessonCategories = [], +}: MfCategoriesSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]); + const selection = useSelection(customerIds); + + return ( + {children} + ); +} + +export function useMfCategoriesSelection(): MfCategoriesSelectionContextValue { + return React.useContext(MfCategoriesSelectionContext); +} + + + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { LoadingButton } from '@mui/lab'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; +import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images'; +import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; + +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; + +import ConfirmDeleteModal from './confirm-delete-modal'; +import { useMfCategoriesSelection } from './mf-categories-selection-context'; +import type { MfCategory } from './type'; + +function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { + return [ + { + formatter: (row): React.JSX.Element => ( + + + + + + {' '} +
+ {row.cat_name} + + slug: {row.cat_name} + +
+
+ +
+ ), + name: 'Name', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} + + + ), + // NOTE: please refer to translation.json here + name: 'word-count', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => { + // eslint-disable-next-line react-hooks/rules-of-hooks + + const mapping = { + active: { + label: 'Active', + icon: ( + + ), + }, + blocked: { label: 'Blocked', icon: }, + pending: { + label: 'Pending', + icon: ( + + ), + }, + NA: { + label: 'NA', + icon: ( + + ), + }, + } as const; + const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; + + return ( + + ); + }, + name: 'Status', + width: '150px', + }, + { + formatter(row) { + return dayjs(row.createdAt).format('MMM D, YYYY'); + }, + name: 'Created at', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + + { + handleDeleteClick(row.id); + }} + > + + + + ), + name: 'Actions', + hideName: true, + width: '100px', + align: 'right', + }, + ]; +} + +export interface LessonCategoriesTableProps { + rows: MfCategory[]; + reloadRows: () => void; +} + +export function MfCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element { + const { t } = useTranslation(['mf_categories']); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useMfCategoriesSelection(); + + const [idToDelete, setIdToDelete] = React.useState(''); + const [open, setOpen] = React.useState(false); + + function handleDeleteClick(testId: string): void { + setOpen(true); + setIdToDelete(testId); + } + + return ( + + + + columns={columns(handleDeleteClick)} + onDeselectAll={deselectAll} + onDeselectOne={(_, row) => { + deselectOne(row.id); + }} + onSelectAll={selectAll} + onSelectOne={(_, row) => { + selectOne(row.id); + }} + rows={rows} + selectable + selected={selected} + /> + {!rows.length ? ( + + + {/* TODO: update this */} + {t('no-lesson-categories-found')} + + + ) : null} + + ); +} +
+ + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +import { Avatar, Divider, MenuItem, Select } from '@mui/material'; +// import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// import axios from 'axios'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import isDevelopment from '@/lib/check-is-development'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; + +import type { CreateFormProps } from './type'; + +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + cat_image: zod.array(zod.any()).optional(), + pos: zod.number().min(1, 'position is required').max(99), + init_answer: zod.string().optional(), + visible: zod.string(), + slug: zod.string().min(1, 'slug-is-required').max(255), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), + // TODO: remove me + type: zod.string().optional(), + isActive: zod.boolean().optional(), + order: zod.number().optional(), +}); + +type Values = zod.infer; + +export const defaultValues = { + cat_name: '', + cat_image: undefined, + pos: 1, + init_answer: '', + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function MfCategoryCreateForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['mf_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_QUIZ_MF_CATEGORIES).create(payload); + + logger.debug(result); + toast.success(t('create.success')); + router.push(paths.dashboard.mf_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('create.failed')); + } finally { + setIsCreating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(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)}
+
+
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +// +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +// +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +// +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; +import FormLoading from '@/components/loading'; + +import ErrorDisplay from '../../error'; +import type { EditFormProps } from './type'; + +// TODO: review this +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + // accept file object when user change image + // accept http string when user not changing image + cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(), + + // position + pos: zod.number().min(1, 'position is required').max(99), + + // it should be a valid JSON + init_answer: zod + .string() + .refine( + (value) => { + try { + JSON.parse(value); + return true; + } catch (error) { + return false; + } + }, + { message: 'init_answer must be a valid JSON' } + ) + .optional(), + visible: zod.string(), + slug: zod.string().min(0, 'slug-is-required').max(255).optional(), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), +}); + +type Values = zod.infer; + +const defaultValues = { + cat_name: '', + cat_image: undefined, + pos: 1, + init_answer: JSON.stringify({}), + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function MfCategoryEditForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['mf_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, + handleSubmit, + formState: { errors }, + setValue, + reset, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + setIsUpdating(true); + + const tempUpdate: EditFormProps = { + cat_name: values.cat_name, + cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null, + pos: values.pos, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + init_answer: JSON.parse(values.init_answer || '{}'), + + visible: values.visible, + slug: values.slug || 'not-defined', + remarks: values.remarks, + description: values.description, + // + // TODO: remove below + type: '', + }; + + try { + const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).update(catId, tempUpdate); + logger.debug(result); + toast.success(t('edit.success')); + router.push(paths.dashboard.mf_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('update.failed')); + } finally { + setIsUpdating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(null); + const avatar = watch('avatar'); + + const handleAvatarChange = React.useCallback( + async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + + if (file) { + const url = await fileToBase64(file); + setValue('avatar', url); + } + }, + [setValue] + ); + + // TODO: need to align with save form + // use trycatch + const [textDescription, setTextDescription] = React.useState(''); + const [textRemarks, setTextRemarks] = React.useState(''); + + // load existing data when user arrive + const loadExistingData = React.useCallback( + async (id: string) => { + setShowLoading(true); + + try { + const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).getOne(id); + + reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); + setTextDescription(result.description); + setTextRemarks(result.remarks); + + if (result.cat_image !== '') { + const fetchResult = await fetch( + `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}` + ); + + const blob = await fetchResult.blob(); + const url = await fileToBase64(blob); + + setValue('avatar', url); + } else { + setValue('avatar', ''); + } + } catch (error) { + logger.error(error); + toast(t('list.error')); + + setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); + } finally { + setShowLoading(false); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [catId] + ); + + React.useEffect(() => { + void loadExistingData(catId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [catId]); + + if (showLoading) return ; + if (showError.show) + return ( + + ); + + return ( +
+ + + } + spacing={4} + > + + {t('edit.basic-info')} + + + + + + + + + + {t('edit.avatar')} + {t('edit.avatarRequirements')} + + + + + + + ( + + {t('edit.cat_name')} + + {errors.cat_name ? {errors.cat_name.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.pos')} + { + field.onChange(parseInt(e.target.value)); + }} + type="number" + /> + {errors.pos ? {errors.pos.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.slug')} + + {errors.slug ? {errors.slug.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.visible')} + + + {errors.visible ? {errors.visible.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.init_answer')} + + {errors.init_answer ? {errors.init_answer.message} : null} + + )} + /> + + + + {/* */} + + {t('edit.detail-information')} + + + { + return ( + + + {t('edit.description')} + + + { + field.onChange({ target: { value: editor.getHTML() } }); + }} + placeholder={t('edit.description.default')} + /> + + + ); + }} + /> + + + ( + + + {t('edit.remarks')} + + + { + field.onChange({ target: { value: editor.getText() } }); + }} + hideToolbar + placeholder={t('edit.remarks.default')} + /> + + + )} + /> + + + + {/* */} + + + + + + + {t('edit.updateButton')} + + + +
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; + +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; +import { Option } from '@/components/core/option'; + +export interface Notification { + id: string; + type: string; + status: 'delivered' | 'pending' | 'failed'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {row.type} + + ), + name: 'Type', + width: '300px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + delivered: { label: 'Delivered', color: 'success' }, + pending: { label: 'Pending', color: 'warning' }, + failed: { label: 'Failed', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface NotificationsProps { + notifications: Notification[]; +} + +export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { + return ( + + + + + } + title="Notifications" + /> + + + + +
+ +
+
+ + + columns={columns} rows={notifications} /> + + +
+
+
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple'; + +import { dayjs } from '@/lib/dayjs'; +import type { ColumnDef } from '@/components/core/data-table'; +import { DataTable } from '@/components/core/data-table'; + +export interface Payment { + currency: string; + amount: number; + invoiceId: string; + status: 'pending' | 'completed' | 'canceled' | 'refunded'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)} + + ), + name: 'Amount', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + pending: { label: 'Pending', color: 'warning' }, + completed: { label: 'Completed', color: 'success' }, + canceled: { label: 'Canceled', color: 'error' }, + refunded: { label: 'Refunded', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + return {row.invoiceId}; + }, + name: 'Invoice ID', + width: '150px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface PaymentsProps { + ordersValue: number; + payments: Payment[]; + refundsValue: number; + totalOrders: number; +} + +export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { + return ( + + }> + Create Payment + + } + avatar={ + + + + } + title="Payments" + /> + + + + } + spacing={3} + sx={{ justifyContent: 'space-between', p: 2 }} + > +
+ + Total orders + + {new Intl.NumberFormat('en-US').format(totalOrders)} +
+
+ + Orders value + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} + +
+
+ + Refunds + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} + +
+
+
+ + + columns={columns} rows={payments} /> + + +
+
+
+ ); +} +
+ + +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; + +export interface Address { + id: string; + country: string; + state: string; + city: string; + zipCode: string; + street: string; + primary?: boolean; +} + +export interface ShippingAddressProps { + address: Address; +} + +export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement { + return ( + + + + + {address.street}, +
+ {address.city}, {address.state}, {address.country}, +
+ {address.zipCode} +
+ + {address.primary ? : } + + +
+
+
+ ); +} +
+ + +export interface MfCategory { + isEmpty?: boolean; + // + id: string; + collectionId: string; + // + cat_name: string; + cat_image_url?: string; + cat_image?: string; + pos: number; + visible: string; + lesson_id: string; + description: string; + remarks: string; + slug: string; + init_answer: any; + // + name: string; + avatar: string; + email: string; + phone: string; + quota: number; + status: 'pending' | 'active' | 'blocked' | 'NA'; + createdAt: Date; +} + +export interface CreateFormProps { + cat_name: string; + cat_image: File[] | null; + pos: number; + init_answer?: string; + visible: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: to remove + type: string; + isActive: boolean; + order: number; + name?: string; + imageUrl?: string; +} + +export interface EditFormProps { + cat_name: string; + cat_image: File[] | null; + pos: number; + init_answer: any; + visible: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: remove below + type: string; +} + +export interface Helloworld { + helloworld: string; +} + + + +import { dayjs } from '@/lib/dayjs'; + +import { CreateFormProps, MfQuestion } from './type'; + +export const defaultMfQuestion: MfQuestion = { + isEmpty: false, + id: 'default-id', + cat_name: 'default-question-name', + cat_image_url: undefined, + cat_image: undefined, + pos: 0, + visible: 'hidden', + lesson_id: 'default-lesson-id', + description: 'default-description', + remarks: 'default-remarks', + slug: '', + init_answer: {}, + // from pocketbase + collectionId: '0000000000', + createdAt: dayjs('2099-01-01').toDate(), + // + name: '', + avatar: '', + email: '', + phone: '', + quota: 0, + status: 'NA', +}; + +// export const LpCategoryCreateFormDefault: CreateFormProps = { +// name: '', +// type: '', +// pos: 1, +// visible: 'visible', +// description: '', +// isActive: true, +// order: 1, +// imageUrl: '', +// }; + +export const emptyLpQuestion: MfQuestion = { + ...defaultMfQuestion, + isEmpty: true, +}; + + + +please review and add translations, e.g. `{t('[word]')}` + +--- + +please help to review the `tsx` file in this folder +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/questions` + +it was clone from +`category`/`categories`, `lp_category`/`lp_categories` +please help to modify to `question`/`questions`, `lp_question`/`lp_questions` + +please also help to modify the name of +`variables`, `constants`, `functions`, `classes`, components's name, paths + +the db fields structures are the same + +do not move the files +do not create directories +keep current folder structure is important + +thanks + + + +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_TYPES } from '@/constants'; +import deleteQuizLPCategories from '@/db/QuizListenings/Delete'; +import { LoadingButton } from '@mui/lab'; +import { Button, Container, Modal, Paper } from '@mui/material'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note'; +import PocketBase from 'pocketbase'; +import { useTranslation } from 'react-i18next'; + +import { logger } from '@/lib/default-logger'; +import { toast } from '@/components/core/toaster'; + +export default function ConfirmDeleteModal({ + open, + setOpen, + idToDelete, + reloadRows, +}: { + open: boolean; + setOpen: (b: boolean) => void; + idToDelete: string; + reloadRows: () => void; +}): React.JSX.Element { + const { t } = useTranslation(); + + // const handleClose = () => setOpen(false); + function handleClose(): void { + setOpen(false); + } + + const [isDeleteing, setIsDeleteing] = React.useState(false); + const style = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + }; + + function handleUserConfirmDelete(): void { + if (idToDelete) { + setIsDeleteing(true); + deleteQuizLPCategories(idToDelete) + .then(() => { + reloadRows(); + handleClose(); + toast(t('delete.success')); + }) + .catch((err) => { + // console.error(err) + logger.error(err); + toast(t('delete.error')); + }) + .finally(() => { + setIsDeleteing(false); + }); + } + } + + return ( +
+ + + + + + + + + + + {t('Delete Lesson Type ?')} + + {t('Are you sure you want to delete lesson type ?')} + + + + + { + handleUserConfirmDelete(); + }} + loading={isDeleteing} + > + {t('Delete')} + + + + + + + + +
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +import { Avatar, Divider, MenuItem, Select } from '@mui/material'; +// import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// import axios from 'axios'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import isDevelopment from '@/lib/check-is-development'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; + +import type { CreateFormProps } from './type'; + +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + cat_image: zod.array(zod.any()).optional(), + pos: zod.number().min(1, 'position is required').max(99), + init_answer: zod.string().optional(), + visible: zod.string(), + slug: zod.string().min(1, 'slug-is-required').max(255), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), + // TODO: remove me + type: zod.string().optional(), + isActive: zod.boolean().optional(), + order: zod.number().optional(), +}); + +type Values = zod.infer; + +export const defaultValues = { + cat_name: '', + cat_image: undefined, + pos: 1, + init_answer: '', + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function MfQuestionCreateForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['lp_categories']); + + const [isCreating, setIsCreating] = React.useState(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_QUIZ_MF_CATEGORIES).create(payload); + + logger.debug(result); + toast.success(t('create.success')); + router.push(paths.dashboard.mf_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('create.failed')); + } finally { + setIsCreating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(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)}
+
+
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +// +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +// +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +// +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; +import FormLoading from '@/components/loading'; + +import ErrorDisplay from '../../error'; +import type { EditFormProps } from './type'; + +// TODO: review this +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + // accept file object when user change image + // accept http string when user not changing image + cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(), + + // position + pos: zod.number().min(1, 'position is required').max(99), + + // it should be a valid JSON + init_answer: zod + .string() + .refine( + (value) => { + try { + JSON.parse(value); + return true; + } catch (error) { + return false; + } + }, + { message: 'init_answer must be a valid JSON' } + ) + .optional(), + visible: zod.string(), + slug: zod.string().min(0, 'slug-is-required').max(255).optional(), + remarks: zod.string().optional(), + description: zod.string().optional(), + // NOTE: for image handling + avatar: zod.string().optional(), +}); + +type Values = zod.infer; + +const defaultValues = { + cat_name: '', + cat_image: undefined, + pos: 1, + init_answer: JSON.stringify({}), + visible: 'hidden', + slug: '', + remarks: '', + description: '', +} satisfies Values; + +export function MfQuestionEditForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['lp_categories']); + + const { cat_id: catId } = useParams<{ cat_id: string }>(); + // + const [isUpdating, setIsUpdating] = React.useState(false); + const [showLoading, setShowLoading] = React.useState(false); + // + const [showError, setShowError] = React.useState({ show: false, detail: '' }); + + const { + control, + handleSubmit, + formState: { errors }, + setValue, + reset, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + setIsUpdating(true); + + const tempUpdate: EditFormProps = { + cat_name: values.cat_name, + cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null, + pos: values.pos, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + init_answer: JSON.parse(values.init_answer || '{}'), + + visible: values.visible, + slug: values.slug || 'not-defined', + remarks: values.remarks, + description: values.description, + // + // TODO: remove below + type: '', + }; + + try { + const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).update(catId, tempUpdate); + logger.debug(result); + toast.success(t('edit.success')); + router.push(paths.dashboard.mf_categories.list); + } catch (error) { + logger.error(error); + toast.error(t('update.failed')); + } finally { + setIsUpdating(false); + } + }, + // t is not necessary here + // eslint-disable-next-line react-hooks/exhaustive-deps + [router] + ); + + const avatarInputRef = React.useRef(null); + const avatar = watch('avatar'); + + const handleAvatarChange = React.useCallback( + async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + + if (file) { + const url = await fileToBase64(file); + setValue('avatar', url); + } + }, + [setValue] + ); + + // TODO: need to align with save form + // use trycatch + const [textDescription, setTextDescription] = React.useState(''); + const [textRemarks, setTextRemarks] = React.useState(''); + + // load existing data when user arrive + const loadExistingData = React.useCallback( + async (id: string) => { + setShowLoading(true); + + try { + const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).getOne(id); + + reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); + setTextDescription(result.description); + setTextRemarks(result.remarks); + + if (result.cat_image !== '') { + const fetchResult = await fetch( + `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}` + ); + + const blob = await fetchResult.blob(); + const url = await fileToBase64(blob); + + setValue('avatar', url); + } else { + setValue('avatar', ''); + } + } catch (error) { + logger.error(error); + toast(t('list.error')); + + setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); + } finally { + setShowLoading(false); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [catId] + ); + + React.useEffect(() => { + void loadExistingData(catId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [catId]); + + if (showLoading) return ; + if (showError.show) + return ( + + ); + + return ( +
+ + + } + spacing={4} + > + + {t('edit.basic-info')} + + + + + + + + + + {t('edit.avatar')} + {t('edit.avatarRequirements')} + + + + + + + ( + + {t('edit.cat_name')} + + {errors.cat_name ? {errors.cat_name.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.pos')} + { + field.onChange(parseInt(e.target.value)); + }} + type="number" + /> + {errors.pos ? {errors.pos.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.slug')} + + {errors.slug ? {errors.slug.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.visible')} + + + {errors.visible ? {errors.visible.message} : null} + + )} + /> + + {/* */} + + ( + + {t('edit.init_answer')} + + {errors.init_answer ? {errors.init_answer.message} : null} + + )} + /> + + + + {/* */} + + {t('edit.detail-information')} + + + { + return ( + + + {t('edit.description')} + + + { + field.onChange({ target: { value: editor.getHTML() } }); + }} + placeholder={t('edit.description.default')} + /> + + + ); + }} + /> + + + ( + + + {t('edit.remarks')} + + + { + field.onChange({ target: { value: editor.getText() } }); + }} + hideToolbar + placeholder={t('edit.remarks.default')} + /> + + + )} + /> + + + + {/* */} + + + + + + + {t('edit.updateButton')} + + + +
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +// import { COL_LESSON_CATEGORIES } from '@/constants'; +import GetAllCount from '@/db/QuizListenings/GetAllCount'; +import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount'; +import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount'; +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import type { SelectChangeEvent } from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; +import { Option } from '@/components/core/option'; + +import { useMfQuestionsSelection } from './mf-questions-selection-context'; +import { MfQuestion } from './type'; + +export interface Filters { + email?: string; + phone?: string; + status?: string; + name?: string; + visible?: string; + type?: string; +} + +export type SortDir = 'asc' | 'desc'; + +export interface MfQuestionsFiltersProps { + filters?: Filters; + sortDir?: SortDir; + fullData: MfQuestion[]; +} + +export function MfQuestionsFilters({ + filters = {}, + sortDir = 'desc', + fullData, +}: MfQuestionsFiltersProps): React.JSX.Element { + const { t } = useTranslation(); + const { email, phone, status, name, visible, type } = filters; + + const [totalCount, setTotalCount] = React.useState(0); + const [visibleCount, setVisibleCount] = React.useState(0); + const [hiddenCount, setHiddenCount] = React.useState(0); + + const router = useRouter(); + + const selection = useMfQuestionsSelection(); + + function getVisible(): number { + return fullData.reduce((count, item: MfQuestion) => { + return item.visible === 'visible' ? count + 1 : count; + }, 0); + } + + function getHidden(): number { + return fullData.reduce((count, item: MfQuestion) => { + return item.visible === 'hidden' ? count + 1 : count; + }, 0); + } + + // The tabs should be generated using API data. + const tabs = [ + { label: t('All'), value: '', count: totalCount }, + // { label: 'Active', value: 'active', count: 3 }, + // { label: 'Pending', value: 'pending', count: 1 }, + // { label: 'Blocked', value: 'blocked', count: 1 }, + { label: t('visible'), value: 'visible', count: visibleCount }, + { label: t('hidden'), value: 'hidden', count: hiddenCount }, + ] as const; + + const updateSearchParams = React.useCallback( + (newFilters: Filters, newSortDir: SortDir): void => { + const searchParams = new URLSearchParams(); + + if (newSortDir === 'asc') { + searchParams.set('sortDir', newSortDir); + } + + if (newFilters.status) { + searchParams.set('status', newFilters.status); + } + + if (newFilters.email) { + searchParams.set('email', newFilters.email); + } + + if (newFilters.phone) { + searchParams.set('phone', newFilters.phone); + } + + if (newFilters.name) { + searchParams.set('name', newFilters.name); + } + + if (newFilters.type) { + searchParams.set('type', newFilters.type); + } + + if (newFilters.visible) { + searchParams.set('visible', newFilters.visible); + } + + // NOTE: modify according to COLLECTION + router.push(`${paths.dashboard.mf_questions.list}?${searchParams.toString()}`); + }, + [router] + ); + + const handleClearFilters = React.useCallback(() => { + updateSearchParams({}, sortDir); + }, [updateSearchParams, sortDir]); + + const handleStatusChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, status: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleVisibleChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, visible: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleNameChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, name: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleTypeChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, type: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleEmailChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, email: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handlePhoneChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, phone: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleSortChange = React.useCallback( + (event: SelectChangeEvent) => { + updateSearchParams(filters, event.target.value as SortDir); + }, + [updateSearchParams, filters] + ); + + React.useEffect(() => { + const fetchCount = async (): Promise => { + try { + const tc = await GetAllCount(); + setTotalCount(tc); + + const vc = await GetVisibleCount(); + setVisibleCount(vc); + + const hc = await GetHiddenCount(); + setHiddenCount(hc); + } catch (error) { + // + } + }; + void fetchCount(); + }, []); + + const hasFilters = status || email || phone || visible || name || type; + + return ( +
+ + {tabs.map((tab) => ( + + } + iconPosition="end" + key={tab.value} + label={tab.label} + sx={{ minHeight: 'auto' }} + tabIndex={0} + value={tab.value} + /> + ))} + + + + + { + handleNameChange(value as string); + }} + onFilterDelete={() => { + handleNameChange(); + }} + popover={} + value={name} + /> + + { + handleTypeChange(value as string); + }} + onFilterDelete={() => { + handleTypeChange(); + }} + popover={} + value={type} + /> + + {hasFilters ? : null} + + {selection.selectedAny ? ( + + + {selection.selected.size} {t('selected')} + + + + ) : null} + + +
+ ); +} + +function TypeFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function NameFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function EmailFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function PhoneFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + const { t } = useTranslation(); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} +
+ + +'use client'; + +import * as React from 'react'; +import TablePagination from '@mui/material/TablePagination'; + +function noop(): void { + return undefined; +} + +interface LessonQuestionsPaginationProps { + count: number; + page: number; + // + setPage: (page: number) => void; + setRowsPerPage: (page: number) => void; + rowsPerPage: number; +} + +export function MfQuestionsPagination({ + count, + page, + // + setPage, + setRowsPerPage, + rowsPerPage, +}: LessonQuestionsPaginationProps): React.JSX.Element { + // You should implement the pagination using a similar logic as the filters. + // Note that when page change, you should keep the filter search params. + const handleChangePage = (event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value)); + // console.log(parseInt(event.target.value)); + }; + + return ( + + ); +} + + + +'use client'; + +import * as React from 'react'; + +// import type { LessonCategory } from '@/types/lesson-type'; +import { useSelection } from '@/hooks/use-selection'; +import type { Selection } from '@/hooks/use-selection'; + +import type { MfQuestion } from './type'; + +function noop(): void { + return undefined; +} + +export interface MfQuestionsSelectionContextValue extends Selection {} + +export const MfQuestionsSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface MfQuestionsSelectionProviderProps { + children: React.ReactNode; + lessonQuestions: MfQuestion[]; +} + +export function MfQuestionsSelectionProvider({ + children, + lessonQuestions = [], +}: MfQuestionsSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => lessonQuestions.map((customer) => customer.id), [lessonQuestions]); + const selection = useSelection(customerIds); + + return ( + {children} + ); +} + +export function useMfQuestionsSelection(): MfQuestionsSelectionContextValue { + return React.useContext(MfQuestionsSelectionContext); +} + + + +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { LoadingButton } from '@mui/lab'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; +import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images'; +import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; + +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; + +import ConfirmDeleteModal from './confirm-delete-modal'; +import { useMfQuestionsSelection } from './mf-questions-selection-context'; +import type { MfQuestion } from './type'; + +function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { + return [ + { + formatter: (row): React.JSX.Element => ( + + + + + + {' '} +
+ {row.cat_name} + + slug: {row.cat_name} + +
+
+ +
+ ), + name: 'Name', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} + + + ), + // NOTE: please refer to translation.json here + name: 'word-count', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => { + // eslint-disable-next-line react-hooks/rules-of-hooks + + const mapping = { + active: { + label: 'Active', + icon: ( + + ), + }, + blocked: { label: 'Blocked', icon: }, + pending: { + label: 'Pending', + icon: ( + + ), + }, + NA: { + label: 'NA', + icon: ( + + ), + }, + } as const; + const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; + + return ( + + ); + }, + name: 'Status', + width: '150px', + }, + { + formatter(row) { + return dayjs(row.createdAt).format('MMM D, YYYY'); + }, + name: 'Created at', + width: '100px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + + { + handleDeleteClick(row.id); + }} + > + + + + ), + name: 'Actions', + hideName: true, + width: '100px', + align: 'right', + }, + ]; +} + +export interface LessonQuestionsTableProps { + rows: MfQuestion[]; + reloadRows: () => void; +} + +export function MfQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps): React.JSX.Element { + const { t } = useTranslation(['lp_categories']); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useMfQuestionsSelection(); + + const [idToDelete, setIdToDelete] = React.useState(''); + const [open, setOpen] = React.useState(false); + + function handleDeleteClick(testId: string): void { + setOpen(true); + setIdToDelete(testId); + } + + return ( + + + + columns={columns(handleDeleteClick)} + onDeselectAll={deselectAll} + onDeselectOne={(_, row) => { + deselectOne(row.id); + }} + onSelectAll={selectAll} + onSelectOne={(_, row) => { + selectOne(row.id); + }} + rows={rows} + selectable + selected={selected} + /> + {!rows.length ? ( + + + {/* TODO: update this */} + {t('no-lesson-categories-found')} + + + ) : null} + + ); +} +
+ + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; + +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; +import { Option } from '@/components/core/option'; + +export interface Notification { + id: string; + type: string; + status: 'delivered' | 'pending' | 'failed'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {row.type} + + ), + name: 'Type', + width: '300px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + delivered: { label: 'Delivered', color: 'success' }, + pending: { label: 'Pending', color: 'warning' }, + failed: { label: 'Failed', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface NotificationsProps { + notifications: Notification[]; +} + +export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { + return ( + + + + + } + title="Notifications" + /> + + + + +
+ +
+
+ + + columns={columns} rows={notifications} /> + + +
+
+
+ ); +} +
+ + +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple'; + +import { dayjs } from '@/lib/dayjs'; +import type { ColumnDef } from '@/components/core/data-table'; +import { DataTable } from '@/components/core/data-table'; + +export interface Payment { + currency: string; + amount: number; + invoiceId: string; + status: 'pending' | 'completed' | 'canceled' | 'refunded'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)} + + ), + name: 'Amount', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + pending: { label: 'Pending', color: 'warning' }, + completed: { label: 'Completed', color: 'success' }, + canceled: { label: 'Canceled', color: 'error' }, + refunded: { label: 'Refunded', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + return {row.invoiceId}; + }, + name: 'Invoice ID', + width: '150px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface PaymentsProps { + ordersValue: number; + payments: Payment[]; + refundsValue: number; + totalOrders: number; +} + +export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { + return ( + + }> + Create Payment + + } + avatar={ + + + + } + title="Payments" + /> + + + + } + spacing={3} + sx={{ justifyContent: 'space-between', p: 2 }} + > +
+ + Total orders + + {new Intl.NumberFormat('en-US').format(totalOrders)} +
+
+ + Orders value + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} + +
+
+ + Refunds + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} + +
+
+
+ + + columns={columns} rows={payments} /> + + +
+
+
+ ); +} +
+ + +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; + +export interface Address { + id: string; + country: string; + state: string; + city: string; + zipCode: string; + street: string; + primary?: boolean; +} + +export interface ShippingAddressProps { + address: Address; +} + +export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement { + return ( + + + + + {address.street}, +
+ {address.city}, {address.state}, {address.country}, +
+ {address.zipCode} +
+ + {address.primary ? : } + + +
+
+
+ ); +} +
+ + +export interface MfQuestion { + isEmpty?: boolean; + // + id: string; + collectionId: string; + // + cat_name: string; + cat_image_url?: string; + cat_image?: string; + pos: number; + visible: string; + lesson_id: string; + description: string; + remarks: string; + slug: string; + init_answer: any; + // + name: string; + avatar: string; + email: string; + phone: string; + quota: number; + status: 'pending' | 'active' | 'blocked' | 'NA'; + createdAt: Date; +} + +export interface CreateFormProps { + cat_name: string; + cat_image: File[] | null; + pos: number; + init_answer?: string; + visible: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: to remove + type: string; + isActive: boolean; + order: number; + name?: string; + imageUrl?: string; +} + +export interface EditFormProps { + cat_name: string; + cat_image: File[] | null; + pos: number; + init_answer: any; + visible: string; + slug: string; + remarks?: string; + description?: string; + // + // TODO: remove below + type: string; +} + +export interface Helloworld { + helloworld: string; +} + + +