diff --git a/002_source/cms/src/app/dashboard/Sample/_repomix.md b/002_source/cms/src/app/dashboard/Sample/_repomix.md new file mode 100644 index 0000000..50aef6b --- /dev/null +++ b/002_source/cms/src/app/dashboard/Sample/_repomix.md @@ -0,0 +1,1120 @@ +This file is a merged representation of the entire codebase, combined into a single document by Repomix. + +# File Summary + +## Purpose +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. + +## File Format +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Multiple file entries, each consisting of: + a. A header with the file path (## File: path/to/file) + b. The full contents of the file in a code block + +## Usage Guidelines +- 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. + +## Notes +- 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) + +## Additional Info + +# Directory Structure +``` +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 +repomix-output.xml +``` + +# Files + +## File: AddressCard/index.tsx +```typescript +'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) => ( + + + + ))} + + + + ); +} +``` + +## File: AddressCard/SampleAddresses.tsx +```typescript +'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', + }, +]; +``` + +## File: BasicDetailCard/index.tsx +```typescript +'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 => ( + + ) + )} + + + ); +} +``` + +## File: Notifications/index.tsx +```typescript +'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(), + }, +]; +``` + +## File: Notifications/type.d.ts +```typescript +export interface Notification { + id: string; + type: string; + status: 'delivered' | 'pending' | 'failed'; + createdAt: Date; +} +``` + +## File: PaymentCard/index.tsx +```typescript +'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 => ( + + ) + )} + + + + + + ); +} +``` + +## File: PaymentCard/SamplePayments.tsx +```typescript +'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(), + }, +]; +``` + +## File: SecurityCard/index.tsx +```typescript +'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')} + +
+
+
+ ); +} +``` + +## File: TitleCard/index.tsx +```typescript +'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')} + +
+
+
+ +
+ + ); +} +``` + +## File: Helloworld.tsx +```typescript +'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; +``` + +## File: repomix-output.xml +```xml +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/_GUIDELINES.md b/002_source/cms/src/app/dashboard/cr/_GUIDELINES.md new file mode 100644 index 0000000..50b3019 --- /dev/null +++ b/002_source/cms/src/app/dashboard/cr/_GUIDELINES.md @@ -0,0 +1,104 @@ +# Connective Revision Guidelines + +## Files and component highlight + +1. `_GUIDELINES.md` - this document +1. categories + +- list (page.tsx), also containing a button to delete record +- read/view ([cat_id]/page.tsx) +- create (create/page.tsx) +- edit/update (edit/[cat_id]/page.tsx) +- optional data for testing(lp-categories-sample-data.tsx) + +1. questions + +- list (page.tsx), also containing a button to delete record +- read/view ([cat_id]/page.tsx) +- create (create/page.tsx) +- edit/update (edit/[cat_id]/page.tsx) +- optional data for testing(lp-categories-sample-data.tsx) + +## Prompt Documents + +Each edit page contains `_PROMPT.md` file that provides guidance for editing. + +## Sample Data + +- `categories/lp-categories-sample-data.tsx`: Categories sample data +- `questions/cr-categories-sample-data.tsx`: Questions sample data + +## Assumptions & Requirements + +1. Using PocketBase to handle ConnectiveRevision records +2. Each ConnectiveRevision record has: + - `id` (autogenerated) + - `collectionId` (autogenerated) + - `collectionName` (autogenerated) + - `created` (autogenerated) + - `updated` (autogenerated) + - `title` (string) + - `description` (string) + - `category` (string) + - `status` (string) + - `priority` (number) + - `dueDate` (string) + - `assignee` (string) + - `reporter` (string) + - `comments` (array) + - `attachments` (array) + - `tags` (array) + - `related` (array) + - `history` (array) + +## Assumption and Requirements + +- the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` +- assume `pb` is located in `@/lib/pb` +- type information defined in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers/type.d.tsx` + +## Component Development Guidelines + +### Requirements + +1. **Single Responsibility Principle**: + +2. **File Organization**: + + - One file per component + - File name should match component name (PascalCase) + - Place components in logical directories based on their purpose + +3. **Type Safety**: + + - Always use TypeScript types/interfaces + - Import types from `@/db/Customers/type.d.tsx` + +4. **PocketBase Integration**: + - Use `pb` instance from `@/lib/pb` + +### Component Example + +```typescript +'use client'; + +import { pb } from '@/lib/pb'; +import { COL_ExampleModel } from '@/constants'; +import type { ExampleType } from '@/db/ExampleModel/type'; + +// common reference to error display, Provide user-friendly error messages +import ErrorDisplay from '@/components/dashboard/error'; + +// declare `Props` explicitively +interface Props { + initialData?: ExampleType; +} + +export default function ExampleForm({ initialData }: Props) { + let { t } = useTranslate(); + + // Render form UI + return <>helloworld +} + +``` diff --git a/002_source/cms/src/app/dashboard/cr/_repomix.md b/002_source/cms/src/app/dashboard/cr/_repomix.md new file mode 100644 index 0000000..1345889 --- /dev/null +++ b/002_source/cms/src/app/dashboard/cr/_repomix.md @@ -0,0 +1,3555 @@ +This file is a merged representation of the entire codebase, combined into a single document by Repomix. + +# File Summary + +## Purpose +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. + +## File Format +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Multiple file entries, each consisting of: + a. A header with the file path (## File: path/to/file) + b. The full contents of the file in a code block + +## Usage Guidelines +- 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. + +## Notes +- 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) + +## Additional Info + +# Directory Structure +``` +categories/ + [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 +questions/ + [cat_id]/ + BasicDetailCard.tsx + page.tsx + TitleCard.tsx + create/ + page.tsx + edit/ + [cat_id]/ + _PROMPT.md + page.tsx + cr-categories-sample-data.tsx + page.tsx +_GUIDELINES.md +repomix-output.xml +``` + +# Files + +## File: categories/[cat_id]/BasicDetailCard.tsx +````typescript +'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 { CrCategory } from '@/components/dashboard/cr/categories/type'; + +export default function BasicDetailCard({ + lpModel: model, + handleEditClick, +}: { + lpModel: CrCategory; + 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 => ( + + ) + )} + + + ); +} +```` + +## File: categories/[cat_id]/page.tsx +````typescript +'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_CR_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 { 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'; +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(defaultCrCategory); + + function handleEditClick() { + router.push(paths.dashboard.cr_categories.edit(showLessonCategory.id)); + } + + React.useEffect(() => { + if (catId) { + pb.collection(COL_QUIZ_CR_CATEGORIES) + .getOne(catId) + .then((model: RecordModel) => { + setShowLessonCategory({ ...defaultCrCategory, ...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')} + +
+ + + +
+ + + + + + + + + + + + + + + +
+
+ ); +} +```` + +## File: categories/[cat_id]/TitleCard.tsx +````typescript +'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 type { CrCategory } from '@/components/dashboard/cr/categories/type'; + +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: CrCategory }): React.JSX.Element { + const { t } = useTranslation(); + + return ( + <> + + + {t('empty')} + +
+ + {lpModel.cat_name} + + } + label={lpModel.visible} + size="small" + variant="outlined" + /> + + + {lpModel.slug} + +
+
+
+ +
+ + ); +} +```` + +## File: categories/create/page.tsx +````typescript +'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 { CrCategoryCreateForm } from '@/components/dashboard/cr/categories/cr-category-create-form'; + +export default function Page(): React.JSX.Element { + // RULES: follow the name of page directory e.g. cr/categoies -> cr_categories + const { t } = useTranslation(['lp_categories']); + + return ( + + + +
+ + + {t('title')} + +
+
+ {t('create.title')} +
+
+ +
+
+ ); +} +```` + +## File: categories/edit/[cat_id]/_PROMPT.md +````markdown +# 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, +```` + +## File: categories/edit/[cat_id]/page.tsx +````typescript +'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 { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-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')} +
+
+ +
+
+ ); +} +```` + +## File: categories/lp-categories-sample-data.tsx +````typescript +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[]; +```` + +## File: categories/page.tsx +````typescript +'use client'; + +// RULES: +// 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_CR_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 { 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 FormLoading from '@/components/loading'; + +export default function Page({ searchParams }: PageProps): React.JSX.Element { + const { t } = useTranslation(['lp_categories']); + // TODO: align to customers page.tsx + 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(0); + // + 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_CR_CATEGORIES) + .getList(currentPage + 1, rowsPerPage, listOption); + const { items, totalItems } = models; + const tempLessonTypes: CrCategory[] = items.map((lt) => { + return { ...defaultCrCategory, ...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.cr_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: CrCategory[], sortDir: 'asc' | 'desc' | undefined): CrCategory[] { + 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: CrCategory[], { email, phone, status, name, visible }: Filters): CrCategory[] { + 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; + // + }; +} +```` + +## File: questions/[cat_id]/BasicDetailCard.tsx +````typescript +'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 => ( + + ) + )} + + + ); +} +```` + +## File: questions/[cat_id]/page.tsx +````typescript +'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_CR_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 { 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'; +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(defaultCrQuestion); + + function handleEditClick() { + router.push(paths.dashboard.cr_questions.edit(showLessonQuestion.id)); + } + + React.useEffect(() => { + if (catId) { + pb.collection(COL_QUIZ_CR_QUESTIONS) + .getOne(catId) + .then((model: RecordModel) => { + setShowLessonQuestion({ ...defaultCrQuestion, ...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')} + +
+ + + +
+ + + + + + + + + + + + + + + +
+
+ ); +} +```` + +## File: questions/[cat_id]/TitleCard.tsx +````typescript +'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} + +
+
+
+ +
+ + ); +} +```` + +## File: questions/create/page.tsx +````typescript +'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 { CrQuestionCreateForm } from '@/components/dashboard/cr/questions/cr-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')} +
+
+ +
+
+ ); +} +```` + +## File: questions/edit/[cat_id]/_PROMPT.md +````markdown +# 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, +```` + +## File: questions/edit/[cat_id]/page.tsx +````typescript +'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 { CrQuestionEditForm } from '@/components/dashboard/cr/questions/cr-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')} +
+
+ +
+
+ ); +} +```` + +## File: questions/cr-categories-sample-data.tsx +````typescript +import { dayjs } from '@/lib/dayjs'; +import { LessonCategory } from '@/components/dashboard/lesson_category/type'; + +export const CrCategoriesSampleData = [ + { + 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[]; +```` + +## File: questions/page.tsx +````typescript +'use client'; + +// RULES: +// 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_CR_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 { 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 [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_CR_QUESTIONS) + .getList(currentPage + 1, rowsPerPage, listOption); + const { items, totalItems } = models; + const tempLessonTypes: CrQuestion[] = items.map((lt) => { + return { ...defaultCrQuestion, ...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.cr_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: CrQuestion[], sortDir: 'asc' | 'desc' | undefined): CrQuestion[] { + 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: CrQuestion[], { email, phone, status, name, visible }: Filters): CrQuestion[] { + 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; + // + }; +} +```` + +## File: _GUIDELINES.md +````markdown +# Connective Revision Guidelines + +## Files and component highlight + +1. `_GUIDELINES.md` - this document +1. categories + +- list (page.tsx), also containing a button to delete record +- read/view ([cat_id]/page.tsx) +- create (create/page.tsx) +- edit/update (edit/[cat_id]/page.tsx) +- optional data for testing(lp-categories-sample-data.tsx) + +1. questions + +- list (page.tsx), also containing a button to delete record +- read/view ([cat_id]/page.tsx) +- create (create/page.tsx) +- edit/update (edit/[cat_id]/page.tsx) +- optional data for testing(lp-categories-sample-data.tsx) + +## Prompt Documents + +Each edit page contains `_PROMPT.md` file that provides guidance for editing. + +## Sample Data + +- `categories/lp-categories-sample-data.tsx`: Categories sample data +- `questions/cr-categories-sample-data.tsx`: Questions sample data + +## Assumptions & Requirements + +1. Using PocketBase to handle ConnectiveRevision records +2. Each ConnectiveRevision record has: + - `id` (autogenerated) + - `collectionId` (autogenerated) + - `collectionName` (autogenerated) + - `created` (autogenerated) + - `updated` (autogenerated) + - `title` (string) + - `description` (string) + - `category` (string) + - `status` (string) + - `priority` (number) + - `dueDate` (string) + - `assignee` (string) + - `reporter` (string) + - `comments` (array) + - `attachments` (array) + - `tags` (array) + - `related` (array) + - `history` (array) + +## Assumption and Requirements + +- the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` +- assume `pb` is located in `@/lib/pb` +- type information defined in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers/type.d.tsx` + +## Component Development Guidelines + +### Requirements + +1. **Single Responsibility Principle**: + +2. **File Organization**: + + - One file per component + - File name should match component name (PascalCase) + - Place components in logical directories based on their purpose + +3. **Type Safety**: + + - Always use TypeScript types/interfaces + - Import types from `@/db/Customers/type.d.tsx` + +4. **PocketBase Integration**: + - Use `pb` instance from `@/lib/pb` + +### Component Example + +```typescript +'use client'; + +import { pb } from '@/lib/pb'; +import { COL_ExampleModel } from '@/constants'; +import type { ExampleType } from '@/db/ExampleModel/type'; + +// common reference to error display, Provide user-friendly error messages +import ErrorDisplay from '@/components/dashboard/error'; + +// declare `Props` explicitively +interface Props { + initialData?: ExampleType; +} + +export default function ExampleForm({ initialData }: Props) { + let { t } = useTranslate(); + + // Render form UI + return <>helloworld +} + +``` +```` + +## File: repomix-output.xml +````xml +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.tsx + edit/ + [cat_id]/ + _PROMPT.md + 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 + cr-categories-sample-data.tsx + page.tsx +_GUIDELINES.md + + + +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 { CrCategory } from '@/components/dashboard/cr/categories/type'; + +export default function BasicDetailCard({ + lpModel: model, + handleEditClick, +}: { + lpModel: CrCategory; + 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_CR_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 { 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'; +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(defaultCrCategory); + + function handleEditClick() { + router.push(paths.dashboard.cr_categories.edit(showLessonCategory.id)); + } + + React.useEffect(() => { + if (catId) { + pb.collection(COL_QUIZ_CR_CATEGORIES) + .getOne(catId) + .then((model: RecordModel) => { + setShowLessonCategory({ ...defaultCrCategory, ...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 type { CrCategory } from '@/components/dashboard/cr/categories/type'; + +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: CrCategory }): 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 { CrCategoryCreateForm } from '@/components/dashboard/cr/categories/cr-category-create-form'; + +export default function Page(): React.JSX.Element { + // RULES: follow the name of page directory e.g. cr/categoies -> cr_categories + 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'; +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 { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-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 cr_categories (QuizCRCategories) +// contain definition to collection only +// +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_QUIZ_CR_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 { 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 FormLoading from '@/components/loading'; + +export default function Page({ searchParams }: PageProps): React.JSX.Element { + const { t } = useTranslation(['lp_categories']); + // TODO: align to customers page.tsx + 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(0); + // + 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_CR_CATEGORIES) + .getList(currentPage + 1, rowsPerPage, listOption); + const { items, totalItems } = models; + const tempLessonTypes: CrCategory[] = items.map((lt) => { + return { ...defaultCrCategory, ...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.cr_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: CrCategory[], sortDir: 'asc' | 'desc' | undefined): CrCategory[] { + 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: CrCategory[], { email, phone, status, name, visible }: Filters): CrCategory[] { + 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_CR_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 { 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'; +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(defaultCrQuestion); + + function handleEditClick() { + router.push(paths.dashboard.cr_questions.edit(showLessonQuestion.id)); + } + + React.useEffect(() => { + if (catId) { + pb.collection(COL_QUIZ_CR_QUESTIONS) + .getOne(catId) + .then((model: RecordModel) => { + setShowLessonQuestion({ ...defaultCrQuestion, ...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 { CrQuestionCreateForm } from '@/components/dashboard/cr/questions/cr-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 { CrQuestionEditForm } from '@/components/dashboard/cr/questions/cr-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 CrCategoriesSampleData = [ + { + 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 cr_questions (QuizCRQuestions) +// contain definition to collection only +// +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_QUIZ_CR_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 { 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 [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_CR_QUESTIONS) + .getList(currentPage + 1, rowsPerPage, listOption); + const { items, totalItems } = models; + const tempLessonTypes: CrQuestion[] = items.map((lt) => { + return { ...defaultCrQuestion, ...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.cr_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: CrQuestion[], sortDir: 'asc' | 'desc' | undefined): CrQuestion[] { + 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: CrQuestion[], { email, phone, status, name, visible }: Filters): CrQuestion[] { + 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; + // + }; +} +
+ + +# Connective Revision Guidelines + +## Files and component highlight + +1. `_GUIDELINES.md` - this document +1. categories + +- list (page.tsx), also containing a button to delete record +- read/view ([cat_id]/page.tsx) +- create (create/page.tsx) +- edit/update (edit/[cat_id]/page.tsx) +- optional data for testing(lp-categories-sample-data.tsx) + +1. questions + +- list (page.tsx), also containing a button to delete record +- read/view ([cat_id]/page.tsx) +- create (create/page.tsx) +- edit/update (edit/[cat_id]/page.tsx) +- optional data for testing(lp-categories-sample-data.tsx) + +## Prompt Documents + +Each edit page contains `_PROMPT.md` file that provides guidance for editing. + +## Sample Data + +- `categories/lp-categories-sample-data.tsx`: Categories sample data +- `questions/cr-categories-sample-data.tsx`: Questions sample data + +## Assumptions & Requirements + +1. Using PocketBase to handle ConnectiveRevision records +2. Each ConnectiveRevision record has: + - `id` (autogenerated) + - `collectionId` (autogenerated) + - `collectionName` (autogenerated) + - `created` (autogenerated) + - `updated` (autogenerated) + - `title` (string) + - `description` (string) + - `category` (string) + - `status` (string) + - `priority` (number) + - `dueDate` (string) + - `assignee` (string) + - `reporter` (string) + - `comments` (array) + - `attachments` (array) + - `tags` (array) + - `related` (array) + - `history` (array) + +## Assumption and Requirements + +- the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` +- assume `pb` is located in `@/lib/pb` +- type information defined in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers/type.d.tsx` + +## Component Development Guidelines + +### Requirements + +1. **Single Responsibility Principle**: + +2. **File Organization**: + + - One file per component + - File name should match component name (PascalCase) + - Place components in logical directories based on their purpose + +3. **Type Safety**: + + - Always use TypeScript types/interfaces + - Import types from `@/db/Customers/type.d.tsx` + +4. **PocketBase Integration**: + - Use `pb` instance from `@/lib/pb` + +### Component Example + +```typescript +'use client'; + +import { pb } from '@/lib/pb'; +import { COL_ExampleModel } from '@/constants'; +import type { ExampleType } from '@/db/ExampleModel/type'; + +// common reference to error display, Provide user-friendly error messages +import ErrorDisplay from '@/components/dashboard/error'; + +// declare `Props` explicitively +interface Props { + initialData?: ExampleType; +} + +export default function ExampleForm({ initialData }: Props) { + let { t } = useTranslate(); + + // Render form UI + return <>helloworld +} + +``` + + +
+```` diff --git a/002_source/cms/src/app/dashboard/cr/repomix-output.xml b/002_source/cms/src/app/dashboard/cr/repomix-output.xml new file mode 100644 index 0000000..9eb3363 --- /dev/null +++ b/002_source/cms/src/app/dashboard/cr/repomix-output.xml @@ -0,0 +1,1772 @@ +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.tsx + edit/ + [cat_id]/ + _PROMPT.md + 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 + cr-categories-sample-data.tsx + page.tsx +_GUIDELINES.md + + + +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 { CrCategory } from '@/components/dashboard/cr/categories/type'; + +export default function BasicDetailCard({ + lpModel: model, + handleEditClick, +}: { + lpModel: CrCategory; + 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_CR_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 { 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'; +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(defaultCrCategory); + + function handleEditClick() { + router.push(paths.dashboard.cr_categories.edit(showLessonCategory.id)); + } + + React.useEffect(() => { + if (catId) { + pb.collection(COL_QUIZ_CR_CATEGORIES) + .getOne(catId) + .then((model: RecordModel) => { + setShowLessonCategory({ ...defaultCrCategory, ...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 type { CrCategory } from '@/components/dashboard/cr/categories/type'; + +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: CrCategory }): 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 { CrCategoryCreateForm } from '@/components/dashboard/cr/categories/cr-category-create-form'; + +export default function Page(): React.JSX.Element { + // RULES: follow the name of page directory e.g. cr/categoies -> cr_categories + 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'; +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 { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-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 cr_categories (QuizCRCategories) +// contain definition to collection only +// +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_QUIZ_CR_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 { 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 FormLoading from '@/components/loading'; + +export default function Page({ searchParams }: PageProps): React.JSX.Element { + const { t } = useTranslation(['lp_categories']); + // TODO: align to customers page.tsx + 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(0); + // + 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_CR_CATEGORIES) + .getList(currentPage + 1, rowsPerPage, listOption); + const { items, totalItems } = models; + const tempLessonTypes: CrCategory[] = items.map((lt) => { + return { ...defaultCrCategory, ...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.cr_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: CrCategory[], sortDir: 'asc' | 'desc' | undefined): CrCategory[] { + 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: CrCategory[], { email, phone, status, name, visible }: Filters): CrCategory[] { + 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_CR_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 { 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'; +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(defaultCrQuestion); + + function handleEditClick() { + router.push(paths.dashboard.cr_questions.edit(showLessonQuestion.id)); + } + + React.useEffect(() => { + if (catId) { + pb.collection(COL_QUIZ_CR_QUESTIONS) + .getOne(catId) + .then((model: RecordModel) => { + setShowLessonQuestion({ ...defaultCrQuestion, ...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 { CrQuestionCreateForm } from '@/components/dashboard/cr/questions/cr-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 { CrQuestionEditForm } from '@/components/dashboard/cr/questions/cr-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 CrCategoriesSampleData = [ + { + 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 cr_questions (QuizCRQuestions) +// contain definition to collection only +// +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_QUIZ_CR_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 { 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 [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_CR_QUESTIONS) + .getList(currentPage + 1, rowsPerPage, listOption); + const { items, totalItems } = models; + const tempLessonTypes: CrQuestion[] = items.map((lt) => { + return { ...defaultCrQuestion, ...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.cr_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: CrQuestion[], sortDir: 'asc' | 'desc' | undefined): CrQuestion[] { + 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: CrQuestion[], { email, phone, status, name, visible }: Filters): CrQuestion[] { + 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; + // + }; +} +
+ + +# Connective Revision Guidelines + +## Files and component highlight + +1. `_GUIDELINES.md` - this document +1. categories + +- list (page.tsx), also containing a button to delete record +- read/view ([cat_id]/page.tsx) +- create (create/page.tsx) +- edit/update (edit/[cat_id]/page.tsx) +- optional data for testing(lp-categories-sample-data.tsx) + +1. questions + +- list (page.tsx), also containing a button to delete record +- read/view ([cat_id]/page.tsx) +- create (create/page.tsx) +- edit/update (edit/[cat_id]/page.tsx) +- optional data for testing(lp-categories-sample-data.tsx) + +## Prompt Documents + +Each edit page contains `_PROMPT.md` file that provides guidance for editing. + +## Sample Data + +- `categories/lp-categories-sample-data.tsx`: Categories sample data +- `questions/cr-categories-sample-data.tsx`: Questions sample data + +## Assumptions & Requirements + +1. Using PocketBase to handle ConnectiveRevision records +2. Each ConnectiveRevision record has: + - `id` (autogenerated) + - `collectionId` (autogenerated) + - `collectionName` (autogenerated) + - `created` (autogenerated) + - `updated` (autogenerated) + - `title` (string) + - `description` (string) + - `category` (string) + - `status` (string) + - `priority` (number) + - `dueDate` (string) + - `assignee` (string) + - `reporter` (string) + - `comments` (array) + - `attachments` (array) + - `tags` (array) + - `related` (array) + - `history` (array) + +## Assumption and Requirements + +- the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` +- assume `pb` is located in `@/lib/pb` +- type information defined in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers/type.d.tsx` + +## Component Development Guidelines + +### Requirements + +1. **Single Responsibility Principle**: + +2. **File Organization**: + + - One file per component + - File name should match component name (PascalCase) + - Place components in logical directories based on their purpose + +3. **Type Safety**: + + - Always use TypeScript types/interfaces + - Import types from `@/db/Customers/type.d.tsx` + +4. **PocketBase Integration**: + - Use `pb` instance from `@/lib/pb` + +### Component Example + +```typescript +'use client'; + +import { pb } from '@/lib/pb'; +import { COL_ExampleModel } from '@/constants'; +import type { ExampleType } from '@/db/ExampleModel/type'; + +// common reference to error display, Provide user-friendly error messages +import ErrorDisplay from '@/components/dashboard/error'; + +// declare `Props` explicitively +interface Props { + initialData?: ExampleType; +} + +export default function ExampleForm({ initialData }: Props) { + let { t } = useTranslate(); + + // Render form UI + return <>helloworld +} + +``` + + +
diff --git a/002_source/cms/src/app/dashboard/customers/customers.tsx b/002_source/cms/src/app/dashboard/customers/SampleCustomers.tsx similarity index 100% rename from 002_source/cms/src/app/dashboard/customers/customers.tsx rename to 002_source/cms/src/app/dashboard/customers/SampleCustomers.tsx diff --git a/002_source/cms/src/app/dashboard/customers/[customerId]/TitleCard.tsx b/002_source/cms/src/app/dashboard/customers/[customerId]/TitleCard.tsx index 9c9f0aa..971bfa7 100644 --- a/002_source/cms/src/app/dashboard/customers/[customerId]/TitleCard.tsx +++ b/002_source/cms/src/app/dashboard/customers/[customerId]/TitleCard.tsx @@ -9,12 +9,14 @@ 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 { Customer } from '@/components/dashboard/customer/type.d'; +import type { Customer } from '@/components/dashboard/customer/type.d'; // import type { CrCategory } from '@/components/dashboard/cr/categories/type'; function getImageUrlFrRecord(record: Customer): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; + // TODO: fix this + // `http://127.0.0.1:8090/api/files/${'record.collectionId'}/${'record.id'}/${'record.cat_image'}`; + return 'getImageUrlFrRecord(helloworld)'; } export default function SampleTitleCard({ lpModel }: { lpModel: Customer }): React.JSX.Element { diff --git a/002_source/cms/src/app/dashboard/customers/_GUIDELINES.md b/002_source/cms/src/app/dashboard/customers/_GUIDELINES.md new file mode 100644 index 0000000..1353901 --- /dev/null +++ b/002_source/cms/src/app/dashboard/customers/_GUIDELINES.md @@ -0,0 +1,49 @@ +# GUIDELINES + +this folder is part of nextjs typescript project and containing page definition for `Customer` / `Customers` record: + +- list (./page.tsx) +- view (./[customerId]/page.tsx) +- create (./create/page.tsx) +- edit (./[customerId]/page.tsx) +- translation provided by react-i18next + +the `@` sign refer to `/002_source/002_source/cms/src` + +## Assumption and Requirements + +- let one file contains one component only. +- type information defined in `/002_source/cms/src/db/Customers/type.d.tsx` +- it mainly consume the db drivers `Customres` in `/002_source/cms/src/db/Customers` + +simple template: + +```typescript +// src/app/dashboard/customers/page.tsx +'use client'; + +// RULES: +// contains list page for customers (Customers) +// contain definition to collection only +// +import statements here ... +... +... +... + +export default function Page({ searchParams }: PageProps): React.JSX.Element { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) + + return ( + <> + {* page content *} + + ) +} + + +interface PageProps { + searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string }; +} +``` diff --git a/002_source/cms/src/app/dashboard/customers/page.tsx b/002_source/cms/src/app/dashboard/customers/page.tsx index c016c5e..4137d42 100644 --- a/002_source/cms/src/app/dashboard/customers/page.tsx +++ b/002_source/cms/src/app/dashboard/customers/page.tsx @@ -25,7 +25,7 @@ import { CustomersPagination } from '@/components/dashboard/customer/customers-p import { CustomersSelectionProvider } from '@/components/dashboard/customer/customers-selection-context'; import { CustomersTable } from '@/components/dashboard/customer/customers-table'; import type { Customer, Filters } from '@/components/dashboard/customer/type.d'; -import { SampleCustomers } from './customers'; +import { SampleCustomers } from './SampleCustomers'; import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; diff --git a/002_source/cms/src/app/dashboard/lp/_repomix.md b/002_source/cms/src/app/dashboard/lp/_repomix.md new file mode 100644 index 0000000..321cc92 --- /dev/null +++ b/002_source/cms/src/app/dashboard/lp/_repomix.md @@ -0,0 +1,3587 @@ +This file is a merged representation of the entire codebase, combined into a single document by Repomix. + +# File Summary + +## Purpose +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. + +## File Format +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Multiple file entries, each consisting of: + a. A header with the file path (## File: path/to/file) + b. The full contents of the file in a code block + +## Usage Guidelines +- 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. + +## Notes +- 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) + +## Additional Info + +# Directory Structure +``` +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 +repomix-output.xml +``` + +# Files + +## File: categories/[cat_id]/BasicDetailCard.tsx +```typescript +'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 => ( + + ) + )} + + + ); +} +``` + +## File: categories/[cat_id]/page.tsx +```typescript +'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')} + +
+ + + +
+ + + + + + + + + + + + + + + +
+
+ ); +} +``` + +## File: categories/[cat_id]/TitleCard.tsx +```typescript +'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} + +
+
+
+ +
+ + ); +} +``` + +## File: categories/create/page copy 2.tsx +```typescript +'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')} +
+
+ +
+
+ ); +} +``` + +## File: categories/create/page copy.tsx +```typescript +'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' })} +
+
+ +
+
+ ); +} +``` + +## File: categories/create/page.tsx +```typescript +'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')} +
+
+ +
+
+ ); +} +``` + +## File: categories/edit/[cat_id]/_PROMPT.md +```markdown +# 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, +``` + +## File: categories/edit/[cat_id]/page copy.tsx +```typescript +'use client'; + +import * as React from 'react'; + +export default function Page(): React.JSX.Element { + return <>helloworld; +} +``` + +## File: categories/edit/[cat_id]/page.tsx +```typescript +'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')} +
+
+ +
+
+ ); +} +``` + +## File: categories/lp-categories-sample-data.tsx +```typescript +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[]; +``` + +## File: categories/page.tsx +```typescript +'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 router = useRouter(); + const { email, phone, sortDir, status, name, visible, type } = searchParams; + 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(0); + // + 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; + // + }; +} +``` + +## File: questions/[cat_id]/BasicDetailCard.tsx +```typescript +'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 => ( + + ) + )} + + + ); +} +``` + +## File: questions/[cat_id]/page.tsx +```typescript +'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')} + +
+ + + +
+ + + + + + + + + + + + + + + +
+
+ ); +} +``` + +## File: questions/[cat_id]/TitleCard.tsx +```typescript +'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} + +
+
+
+ +
+ + ); +} +``` + +## File: questions/create/page.tsx +```typescript +'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')} +
+
+ +
+
+ ); +} +``` + +## File: questions/edit/[cat_id]/_PROMPT.md +```markdown +# 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, +``` + +## File: questions/edit/[cat_id]/page.tsx +```typescript +'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')} +
+
+ +
+
+ ); +} +``` + +## File: questions/lp-categories-sample-data.tsx +```typescript +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[]; +``` + +## File: questions/page.tsx +```typescript +'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; + // + }; +} +``` + +## File: repomix-output.xml +```xml +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 router = useRouter(); + const { email, phone, sortDir, status, name, visible, type } = searchParams; + 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(0); + // + 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/lp/repomix-output.xml b/002_source/cms/src/app/dashboard/lp/repomix-output.xml new file mode 100644 index 0000000..a6b0914 --- /dev/null +++ b/002_source/cms/src/app/dashboard/lp/repomix-output.xml @@ -0,0 +1,1787 @@ +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 router = useRouter(); + const { email, phone, sortDir, status, name, visible, type } = searchParams; + 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(0); + // + 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/mf/_repomix.md b/002_source/cms/src/app/dashboard/mf/_repomix.md new file mode 100644 index 0000000..7e31251 --- /dev/null +++ b/002_source/cms/src/app/dashboard/mf/_repomix.md @@ -0,0 +1,3336 @@ +This file is a merged representation of the entire codebase, combined into a single document by Repomix. + +# File Summary + +## Purpose +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. + +## File Format +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Multiple file entries, each consisting of: + a. A header with the file path (## File: path/to/file) + b. The full contents of the file in a code block + +## Usage Guidelines +- 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. + +## Notes +- 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) + +## Additional Info + +# Directory Structure +``` +categories/ + [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 +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 +repomix-output.xml +``` + +# Files + +## File: categories/[cat_id]/BasicDetailCard.tsx +```typescript +'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 type { MfCategory } from '@/components/dashboard/mf/categories/type'; + +export default function BasicDetailCard({ + lpModel: model, + handleEditClick, +}: { + lpModel: MfCategory; + 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 => ( + + ) + )} + + + ); +} +``` + +## File: categories/[cat_id]/page.tsx +```typescript +'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_MF_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 { defaultMfCategory } from '@/components/dashboard/mf/categories/_constants.ts'; +import { Notifications } from '@/components/dashboard/mf/categories/notifications'; +import type { MfCategory } from '@/components/dashboard/mf/categories/type'; +import FormLoading from '@/components/loading'; + +import BasicDetailCard from './BasicDetailCard'; +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(defaultMfCategory); + + function handleEditClick() { + router.push(paths.dashboard.mf_categories.edit(showLessonCategory.id)); + } + + React.useEffect(() => { + if (catId) { + pb.collection(COL_QUIZ_MF_CATEGORIES) + .getOne(catId) + .then((model: RecordModel) => { + setShowLessonCategory({ ...defaultMfCategory, ...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')} + +
+ + + +
+ + + + + + + + + + + + + + + +
+
+ ); +} +``` + +## File: categories/[cat_id]/TitleCard.tsx +```typescript +'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 type { MfCategory } from '@/components/dashboard/mf/categories/type'; + +function getImageUrlFrRecord(record: MfCategory): string { + return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; +} + +export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element { + const { t } = useTranslation(); + + return ( + <> + + + {t('empty')} + +
+ + {lpModel.cat_name} + + } + label={lpModel.visible} + size="small" + variant="outlined" + /> + + + {lpModel.slug} + +
+
+
+ +
+ + ); +} +``` + +## File: categories/create/page.tsx +```typescript +'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 { MfCategoryCreateForm } from '@/components/dashboard/mf/categories/mf-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')} +
+
+ +
+
+ ); +} +``` + +## File: categories/edit/[cat_id]/_PROMPT.md +```markdown +# 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, +``` + +## File: categories/edit/[cat_id]/page.tsx +```typescript +'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 { MfCategoryEditForm } from '@/components/dashboard/mf/categories/mf-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')} +
+
+ +
+
+ ); +} +``` + +## File: categories/lp-categories-sample-data.tsx +```typescript +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[]; +``` + +## File: categories/page.tsx +```typescript +'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_MF_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 { defaultMfCategory } from '@/components/dashboard/mf/categories/_constants'; +import { MfCategoriesFilters } from '@/components/dashboard/mf/categories/mf-categories-filters'; +import type { Filters } from '@/components/dashboard/mf/categories/mf-categories-filters'; +import { MfCategoriesPagination } from '@/components/dashboard/mf/categories/mf-categories-pagination'; +import { MfCategoriesSelectionProvider } from '@/components/dashboard/mf/categories/mf-categories-selection-context'; +import { MfCategoriesTable } from '@/components/dashboard/mf/categories/mf-categories-table'; +import type { MfCategory } from '@/components/dashboard/mf/categories/type'; +import FormLoading from '@/components/loading'; + +export default function Page({ searchParams }: PageProps): React.JSX.Element { + const { t } = useTranslation(['mf_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_MF_CATEGORIES) + .getList(currentPage + 1, rowsPerPage, listOption); + const { items, totalItems } = models; + const tempLessonTypes: MfCategory[] = items.map((lt) => { + return { ...defaultMfCategory, ...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.mf_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: MfCategory[], sortDir: 'asc' | 'desc' | undefined): MfCategory[] { + 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: MfCategory[], { email, phone, status, name, visible }: Filters): MfCategory[] { + 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; + // + }; +} +``` + +## File: questions/[cat_id]/BasicDetailCard.tsx +```typescript +'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 { MfCategory } from '@/components/dashboard/mf/categories/type'; + +export default function BasicDetailCard({ + lpModel: model, + handleEditClick, +}: { + lpModel: MfCategory; + 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 => ( + + ) + )} + + + ); +} +``` + +## File: questions/[cat_id]/page.tsx +```typescript +'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_MF_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 { defaultMfQuestion } from '@/components/dashboard/mf/questions/_constants.ts'; +import { Notifications } from '@/components/dashboard/mf/questions/notifications'; +import type { MfQuestion } from '@/components/dashboard/mf/questions/type'; +import FormLoading from '@/components/loading'; + +import BasicDetailCard from './BasicDetailCard'; +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(defaultMfQuestion); + + function handleEditClick() { + router.push(paths.dashboard.mf_questions.edit(showLessonQuestion.id)); + } + + React.useEffect(() => { + if (catId) { + pb.collection(COL_QUIZ_MF_QUESTIONS) + .getOne(catId) + .then((model: RecordModel) => { + setShowLessonQuestion({ ...defaultMfQuestion, ...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')} + +
+ + + +
+ + + + + + + + + + + + + + + +
+
+ ); +} +``` + +## File: questions/[cat_id]/TitleCard.tsx +```typescript +'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 type { MfCategory } from '@/components/dashboard/mf/categories/type'; + +function getImageUrlFrRecord(record: MfCategory): string { + return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; +} + +export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element { + const { t } = useTranslation(); + + return ( + <> + + + {t('empty')} + +
+ + {lpModel.cat_name} + + } + label={lpModel.visible} + size="small" + variant="outlined" + /> + + + {lpModel.slug} + +
+
+
+ +
+ + ); +} +``` + +## File: questions/create/page.tsx +```typescript +'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 { MfQuestionCreateForm } from '@/components/dashboard/mf/questions/mf-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')} +
+
+ +
+
+ ); +} +``` + +## File: questions/edit/[cat_id]/_PROMPT.md +```markdown +# 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, +``` + +## File: questions/edit/[cat_id]/page.tsx +```typescript +'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 { MfQuestionEditForm } from '@/components/dashboard/mf/questions/mf-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')} +
+
+ +
+
+ ); +} +``` + +## File: questions/lp-categories-sample-data.tsx +```typescript +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[]; +``` + +## File: questions/page.tsx +```typescript +'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_MF_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 { defaultMfQuestion } from '@/components/dashboard/mf/questions/_constants'; +import { MfQuestionsFilters } from '@/components/dashboard/mf/questions/mf-questions-filters'; +import type { Filters } from '@/components/dashboard/mf/questions/mf-questions-filters'; +import { MfQuestionsPagination } from '@/components/dashboard/mf/questions/mf-questions-pagination'; +import { MfQuestionsSelectionProvider } from '@/components/dashboard/mf/questions/mf-questions-selection-context'; +import { MfQuestionsTable } from '@/components/dashboard/mf/questions/mf-questions-table'; +import type { MfQuestion } from '@/components/dashboard/mf/questions/type'; +import FormLoading from '@/components/loading'; + +export default function Page({ searchParams }: PageProps): React.JSX.Element { + const { t } = useTranslation(['mf_question']); + 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_MF_QUESTIONS) + .getList(currentPage + 1, rowsPerPage, listOption); + const { items, totalItems } = models; + const tempLessonTypes: MfQuestion[] = items.map((lt) => { + return { ...defaultMfQuestion, ...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: MfQuestion[], sortDir: 'asc' | 'desc' | undefined): MfQuestion[] { + 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: MfQuestion[], { email, phone, status, name, visible }: Filters): MfQuestion[] { + 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; + // + }; +} +``` + +## File: repomix-output.xml +```xml +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.tsx + edit/ + [cat_id]/ + _PROMPT.md + 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 type { MfCategory } from '@/components/dashboard/mf/categories/type'; + +export default function BasicDetailCard({ + lpModel: model, + handleEditClick, +}: { + lpModel: MfCategory; + 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_MF_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 { defaultMfCategory } from '@/components/dashboard/mf/categories/_constants.ts'; +import { Notifications } from '@/components/dashboard/mf/categories/notifications'; +import type { MfCategory } from '@/components/dashboard/mf/categories/type'; +import FormLoading from '@/components/loading'; + +import BasicDetailCard from './BasicDetailCard'; +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(defaultMfCategory); + + function handleEditClick() { + router.push(paths.dashboard.mf_categories.edit(showLessonCategory.id)); + } + + React.useEffect(() => { + if (catId) { + pb.collection(COL_QUIZ_MF_CATEGORIES) + .getOne(catId) + .then((model: RecordModel) => { + setShowLessonCategory({ ...defaultMfCategory, ...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 type { MfCategory } from '@/components/dashboard/mf/categories/type'; + +function getImageUrlFrRecord(record: MfCategory): string { + return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; +} + +export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): 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 { MfCategoryCreateForm } from '@/components/dashboard/mf/categories/mf-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'; +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 { MfCategoryEditForm } from '@/components/dashboard/mf/categories/mf-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_MF_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 { defaultMfCategory } from '@/components/dashboard/mf/categories/_constants'; +import { MfCategoriesFilters } from '@/components/dashboard/mf/categories/mf-categories-filters'; +import type { Filters } from '@/components/dashboard/mf/categories/mf-categories-filters'; +import { MfCategoriesPagination } from '@/components/dashboard/mf/categories/mf-categories-pagination'; +import { MfCategoriesSelectionProvider } from '@/components/dashboard/mf/categories/mf-categories-selection-context'; +import { MfCategoriesTable } from '@/components/dashboard/mf/categories/mf-categories-table'; +import type { MfCategory } from '@/components/dashboard/mf/categories/type'; +import FormLoading from '@/components/loading'; + +export default function Page({ searchParams }: PageProps): React.JSX.Element { + const { t } = useTranslation(['mf_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_MF_CATEGORIES) + .getList(currentPage + 1, rowsPerPage, listOption); + const { items, totalItems } = models; + const tempLessonTypes: MfCategory[] = items.map((lt) => { + return { ...defaultMfCategory, ...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.mf_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: MfCategory[], sortDir: 'asc' | 'desc' | undefined): MfCategory[] { + 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: MfCategory[], { email, phone, status, name, visible }: Filters): MfCategory[] { + 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 { MfCategory } from '@/components/dashboard/mf/categories/type'; + +export default function BasicDetailCard({ + lpModel: model, + handleEditClick, +}: { + lpModel: MfCategory; + 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_MF_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 { defaultMfQuestion } from '@/components/dashboard/mf/questions/_constants.ts'; +import { Notifications } from '@/components/dashboard/mf/questions/notifications'; +import type { MfQuestion } from '@/components/dashboard/mf/questions/type'; +import FormLoading from '@/components/loading'; + +import BasicDetailCard from './BasicDetailCard'; +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(defaultMfQuestion); + + function handleEditClick() { + router.push(paths.dashboard.mf_questions.edit(showLessonQuestion.id)); + } + + React.useEffect(() => { + if (catId) { + pb.collection(COL_QUIZ_MF_QUESTIONS) + .getOne(catId) + .then((model: RecordModel) => { + setShowLessonQuestion({ ...defaultMfQuestion, ...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 type { MfCategory } from '@/components/dashboard/mf/categories/type'; + +function getImageUrlFrRecord(record: MfCategory): string { + return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; +} + +export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): 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 { MfQuestionCreateForm } from '@/components/dashboard/mf/questions/mf-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 { MfQuestionEditForm } from '@/components/dashboard/mf/questions/mf-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_MF_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 { defaultMfQuestion } from '@/components/dashboard/mf/questions/_constants'; +import { MfQuestionsFilters } from '@/components/dashboard/mf/questions/mf-questions-filters'; +import type { Filters } from '@/components/dashboard/mf/questions/mf-questions-filters'; +import { MfQuestionsPagination } from '@/components/dashboard/mf/questions/mf-questions-pagination'; +import { MfQuestionsSelectionProvider } from '@/components/dashboard/mf/questions/mf-questions-selection-context'; +import { MfQuestionsTable } from '@/components/dashboard/mf/questions/mf-questions-table'; +import type { MfQuestion } from '@/components/dashboard/mf/questions/type'; +import FormLoading from '@/components/loading'; + +export default function Page({ searchParams }: PageProps): React.JSX.Element { + const { t } = useTranslation(['mf_question']); + 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_MF_QUESTIONS) + .getList(currentPage + 1, rowsPerPage, listOption); + const { items, totalItems } = models; + const tempLessonTypes: MfQuestion[] = items.map((lt) => { + return { ...defaultMfQuestion, ...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: MfQuestion[], sortDir: 'asc' | 'desc' | undefined): MfQuestion[] { + 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: MfQuestion[], { email, phone, status, name, visible }: Filters): MfQuestion[] { + 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/students/[customerId]/page.tsx b/002_source/cms/src/app/dashboard/students/[customerId]/page.tsx deleted file mode 100644 index edf064e..0000000 --- a/002_source/cms/src/app/dashboard/students/[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/students/create/page.tsx b/002_source/cms/src/app/dashboard/students/create/page.tsx deleted file mode 100644 index a0460ab..0000000 --- a/002_source/cms/src/app/dashboard/students/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/students/page.tsx b/002_source/cms/src/app/dashboard/students/page.tsx deleted file mode 100644 index 851e6ff..0000000 --- a/002_source/cms/src/app/dashboard/students/page.tsx +++ /dev/null @@ -1,268 +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/type.d'; - -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/teachers/[customerId]/page.tsx b/002_source/cms/src/app/dashboard/teachers/[customerId]/page.tsx deleted file mode 100644 index edf064e..0000000 --- a/002_source/cms/src/app/dashboard/teachers/[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/teachers/create/page.tsx b/002_source/cms/src/app/dashboard/teachers/create/page.tsx deleted file mode 100644 index a0460ab..0000000 --- a/002_source/cms/src/app/dashboard/teachers/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/teachers/page.tsx b/002_source/cms/src/app/dashboard/teachers/page.tsx deleted file mode 100644 index 851e6ff..0000000 --- a/002_source/cms/src/app/dashboard/teachers/page.tsx +++ /dev/null @@ -1,268 +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/type.d'; - -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/components/dashboard/cf_categories/customer-create-form.tsx b/002_source/cms/src/components/dashboard/cf_categories/customer-create-form.tsx deleted file mode 100644 index 7be8fc8..0000000 --- a/002_source/cms/src/components/dashboard/cf_categories/customer-create-form.tsx +++ /dev/null @@ -1,398 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useRouter } from 'next/navigation'; -import { zodResolver } from '@hookform/resolvers/zod'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardActions from '@mui/material/CardActions'; -import CardContent from '@mui/material/CardContent'; -import Checkbox from '@mui/material/Checkbox'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import FormHelperText from '@mui/material/FormHelperText'; -import InputLabel from '@mui/material/InputLabel'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Select from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import Grid from '@mui/material/Unstable_Grid2'; -import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; -import { Controller, useForm } from 'react-hook-form'; -import { z as zod } from 'zod'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { Option } from '@/components/core/option'; -import { toast } from '@/components/core/toaster'; - -function fileToBase64(file: Blob): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - resolve(reader.result as string); - }; - reader.onerror = () => { - reject(new Error('Error converting file to base64')); - }; - }); -} - -const schema = zod.object({ - avatar: zod.string().optional(), - name: zod.string().min(1, 'Name is required').max(255), - email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255), - phone: zod.string().min(1, 'Phone is required').max(15), - company: zod.string().max(255), - billingAddress: zod.object({ - country: zod.string().min(1, 'Country is required').max(255), - state: zod.string().min(1, 'State is required').max(255), - city: zod.string().min(1, 'City is required').max(255), - zipCode: zod.string().min(1, 'Zip code is required').max(255), - line1: zod.string().min(1, 'Street line 1 is required').max(255), - line2: zod.string().max(255).optional(), - }), - taxId: zod.string().max(255).optional(), - timezone: zod.string().min(1, 'Timezone is required').max(255), - language: zod.string().min(1, 'Language is required').max(255), - currency: zod.string().min(1, 'Currency is required').max(255), -}); - -type Values = zod.infer; - -const defaultValues = { - avatar: '', - name: '', - email: '', - phone: '', - company: '', - billingAddress: { country: '', state: '', city: '', zipCode: '', line1: '', line2: '' }, - taxId: '', - timezone: 'new_york', - language: 'en', - currency: 'USD', -} satisfies Values; - -export function CustomerCreateForm(): React.JSX.Element { - const router = useRouter(); - - const { - control, - handleSubmit, - formState: { errors }, - setValue, - watch, - } = useForm({ defaultValues, resolver: zodResolver(schema) }); - - const onSubmit = React.useCallback( - async (_: Values): Promise => { - try { - // Make API request - toast.success('Customer updated'); - router.push(paths.dashboard.customers.details('1')); - } catch (err) { - logger.error(err); - toast.error('Something went wrong!'); - } - }, - [router] - ); - - const avatarInputRef = React.useRef(null); - const avatar = watch('avatar'); - - const handleAvatarChange = React.useCallback( - async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - - if (file) { - const url = await fileToBase64(file); - setValue('avatar', url); - } - }, - [setValue] - ); - - return ( -
- - - } spacing={4}> - - Account information - - - - - - - - - - Avatar - Min 400x400px, PNG or JPEG - - - - - - - ( - - Name - - {errors.name ? {errors.name.message} : null} - - )} - /> - - - ( - - Email address - - {errors.email ? {errors.email.message} : null} - - )} - /> - - - ( - - Phone number - - {errors.phone ? {errors.phone.message} : null} - - )} - /> - - - ( - - Company - - {errors.company ? {errors.company.message} : null} - - )} - /> - - - - - Billing information - - - ( - - Country - - {errors.billingAddress?.country ? ( - {errors.billingAddress?.country?.message} - ) : null} - - )} - /> - - - ( - - State - - {errors.billingAddress?.state ? ( - {errors.billingAddress?.state?.message} - ) : null} - - )} - /> - - - ( - - City - - {errors.billingAddress?.city ? ( - {errors.billingAddress?.city?.message} - ) : null} - - )} - /> - - - ( - - Zip code - - {errors.billingAddress?.zipCode ? ( - {errors.billingAddress?.zipCode?.message} - ) : null} - - )} - /> - - - ( - - Address - - {errors.billingAddress?.line1 ? ( - {errors.billingAddress?.line1?.message} - ) : null} - - )} - /> - - - ( - - Tax ID - - {errors.taxId ? {errors.taxId.message} : null} - - )} - /> - - - - - Shipping information - } label="Same as billing address" /> - - - Additional information - - - ( - - Timezone - - {errors.timezone ? {errors.timezone.message} : null} - - )} - /> - - - ( - - Language - - {errors.language ? {errors.language.message} : null} - - )} - /> - - - ( - - Currency - - {errors.currency ? {errors.currency.message} : null} - - )} - /> - - - - - - - - - - -
- ); -} diff --git a/002_source/cms/src/components/dashboard/cf_categories/customers-filters.tsx b/002_source/cms/src/components/dashboard/cf_categories/customers-filters.tsx deleted file mode 100644 index 1567e3b..0000000 --- a/002_source/cms/src/components/dashboard/cf_categories/customers-filters.tsx +++ /dev/null @@ -1,241 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import Button from '@mui/material/Button'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Select from '@mui/material/Select'; -import type { SelectChangeEvent } from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Tab from '@mui/material/Tab'; -import Tabs from '@mui/material/Tabs'; -import Typography from '@mui/material/Typography'; - -import { paths } from '@/paths'; -import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; -import { Option } from '@/components/core/option'; - -import { useCustomersSelection } from './customers-selection-context'; - -// The tabs should be generated using API data. -const tabs = [ - { label: 'All', value: '', count: 5 }, - { label: 'Active', value: 'active', count: 3 }, - { label: 'Pending', value: 'pending', count: 1 }, - { label: 'Blocked', value: 'blocked', count: 1 }, -] as const; - -export interface Filters { - email?: string; - phone?: string; - status?: string; -} - -export type SortDir = 'asc' | 'desc'; - -export interface CustomersFiltersProps { - filters?: Filters; - sortDir?: SortDir; -} - -export function CustomersFilters({ filters = {}, sortDir = 'desc' }: CustomersFiltersProps): React.JSX.Element { - const { email, phone, status } = filters; - - const router = useRouter(); - - const selection = useCustomersSelection(); - - const updateSearchParams = React.useCallback( - (newFilters: Filters, newSortDir: SortDir): void => { - const searchParams = new URLSearchParams(); - - if (newSortDir === 'asc') { - searchParams.set('sortDir', newSortDir); - } - - if (newFilters.status) { - searchParams.set('status', newFilters.status); - } - - if (newFilters.email) { - searchParams.set('email', newFilters.email); - } - - if (newFilters.phone) { - searchParams.set('phone', newFilters.phone); - } - - router.push(`${paths.dashboard.customers.list}?${searchParams.toString()}`); - }, - [router] - ); - - const handleClearFilters = React.useCallback(() => { - updateSearchParams({}, sortDir); - }, [updateSearchParams, sortDir]); - - const handleStatusChange = React.useCallback( - (_: React.SyntheticEvent, value: string) => { - updateSearchParams({ ...filters, status: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleEmailChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, email: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handlePhoneChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, phone: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleSortChange = React.useCallback( - (event: SelectChangeEvent) => { - updateSearchParams(filters, event.target.value as SortDir); - }, - [updateSearchParams, filters] - ); - - const hasFilters = status || email || phone; - - return ( -
- - {tabs.map((tab) => ( - } - iconPosition="end" - key={tab.value} - label={tab.label} - sx={{ minHeight: 'auto' }} - tabIndex={0} - value={tab.value} - /> - ))} - - - - - { - handleEmailChange(value as string); - }} - onFilterDelete={() => { - handleEmailChange(); - }} - popover={} - value={email} - /> - { - handlePhoneChange(value as string); - }} - onFilterDelete={() => { - handlePhoneChange(); - }} - popover={} - value={phone} - /> - {hasFilters ? : null} - - {selection.selectedAny ? ( - - - {selection.selected.size} selected - - - - ) : null} - - -
- ); -} - -function EmailFilterPopover(): React.JSX.Element { - 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 PhoneFilterPopover(): React.JSX.Element { - 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} - /> - - - - ); -} diff --git a/002_source/cms/src/components/dashboard/cf_categories/customers-pagination.tsx b/002_source/cms/src/components/dashboard/cf_categories/customers-pagination.tsx deleted file mode 100644 index ab01272..0000000 --- a/002_source/cms/src/components/dashboard/cf_categories/customers-pagination.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import * as React from 'react'; -import TablePagination from '@mui/material/TablePagination'; - -function noop(): void { - return undefined; -} - -interface CustomersPaginationProps { - count: number; - page: number; -} - -export function CustomersPagination({ count, page }: CustomersPaginationProps): React.JSX.Element { - // You should implement the pagination using a similar logic as the filters. - // Note that when page change, you should keep the filter search params. - - return ( - - ); -} diff --git a/002_source/cms/src/components/dashboard/cf_categories/customers-selection-context.tsx b/002_source/cms/src/components/dashboard/cf_categories/customers-selection-context.tsx deleted file mode 100644 index 023dbc0..0000000 --- a/002_source/cms/src/components/dashboard/cf_categories/customers-selection-context.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import * as React from 'react'; - -import { useSelection } from '@/hooks/use-selection'; -import type { Selection } from '@/hooks/use-selection'; - -import type { Customer } from './customers-table'; - -function noop(): void { - return undefined; -} - -export interface CustomersSelectionContextValue extends Selection {} - -export const CustomersSelectionContext = React.createContext({ - deselectAll: noop, - deselectOne: noop, - selectAll: noop, - selectOne: noop, - selected: new Set(), - selectedAny: false, - selectedAll: false, -}); - -interface CustomersSelectionProviderProps { - children: React.ReactNode; - customers: Customer[]; -} - -export function CustomersSelectionProvider({ - children, - customers = [], -}: CustomersSelectionProviderProps): React.JSX.Element { - const customerIds = React.useMemo(() => customers.map((customer) => customer.id), [customers]); - const selection = useSelection(customerIds); - - return {children}; -} - -export function useCustomersSelection(): CustomersSelectionContextValue { - return React.useContext(CustomersSelectionContext); -} diff --git a/002_source/cms/src/components/dashboard/cf_categories/customers-table.tsx b/002_source/cms/src/components/dashboard/cf_categories/customers-table.tsx deleted file mode 100644 index bf9b01a..0000000 --- a/002_source/cms/src/components/dashboard/cf_categories/customers-table.tsx +++ /dev/null @@ -1,139 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Chip from '@mui/material/Chip'; -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 { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; -import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; -import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; - -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 { useCustomersSelection } from './customers-selection-context'; - -export interface Customer { - id: string; - name: string; - avatar?: string; - email: string; - phone?: string; - quota: number; - status: 'pending' | 'active' | 'blocked'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {' '} -
- - {row.name} - - - {row.email} - -
-
- ), - name: 'Name', - width: '250px', - }, - { - formatter: (row): React.JSX.Element => ( - - - - {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} - - - ), - name: 'Quota', - width: '250px', - }, - { field: 'phone', name: 'Phone number', width: '150px' }, - { - formatter(row) { - return dayjs(row.createdAt).format('MMM D, YYYY h:mm A'); - }, - name: 'Created at', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - active: { label: 'Active', icon: }, - blocked: { label: 'Blocked', icon: }, - pending: { label: 'Pending', icon: }, - } as const; - const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; - - return ; - }, - name: 'Status', - width: '150px', - }, - { - formatter: (): React.JSX.Element => ( - - - - ), - name: 'Actions', - hideName: true, - width: '100px', - align: 'right', - }, -] satisfies ColumnDef[]; - -export interface CustomersTableProps { - rows: Customer[]; -} - -export function CustomersTable({ rows }: CustomersTableProps): React.JSX.Element { - const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCustomersSelection(); - - return ( - - - columns={columns} - onDeselectAll={deselectAll} - onDeselectOne={(_, row) => { - deselectOne(row.id); - }} - onSelectAll={selectAll} - onSelectOne={(_, row) => { - selectOne(row.id); - }} - rows={rows} - selectable - selected={selected} - /> - {!rows.length ? ( - - - No customers found - - - ) : null} - - ); -} diff --git a/002_source/cms/src/components/dashboard/cf_categories/helloworld.tsx b/002_source/cms/src/components/dashboard/cf_categories/helloworld.tsx deleted file mode 100644 index 3989cb1..0000000 --- a/002_source/cms/src/components/dashboard/cf_categories/helloworld.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const helloworld = 'helloworld'; - -export { helloworld }; diff --git a/002_source/cms/src/components/dashboard/cf_categories/notifications.tsx b/002_source/cms/src/components/dashboard/cf_categories/notifications.tsx deleted file mode 100644 index a6c16bd..0000000 --- a/002_source/cms/src/components/dashboard/cf_categories/notifications.tsx +++ /dev/null @@ -1,101 +0,0 @@ -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import Chip from '@mui/material/Chip'; -import Select from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; - -import { dayjs } from '@/lib/dayjs'; -import { DataTable } from '@/components/core/data-table'; -import type { ColumnDef } from '@/components/core/data-table'; -import { Option } from '@/components/core/option'; - -export interface Notification { - id: string; - type: string; - status: 'delivered' | 'pending' | 'failed'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {row.type} - - ), - name: 'Type', - width: '300px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - delivered: { label: 'Delivered', color: 'success' }, - pending: { label: 'Pending', color: 'warning' }, - failed: { label: 'Failed', color: 'error' }, - } as const; - const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; - - return ; - }, - name: 'Status', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => ( - - {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} - - ), - name: 'Date', - align: 'right', - }, -] satisfies ColumnDef[]; - -export interface NotificationsProps { - notifications: Notification[]; -} - -export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { - return ( - - - - - } - title="Notifications" - /> - - - - -
- -
-
- - - columns={columns} rows={notifications} /> - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/cf_categories/payments.tsx b/002_source/cms/src/components/dashboard/cf_categories/payments.tsx deleted file mode 100644 index 0420d32..0000000 --- a/002_source/cms/src/components/dashboard/cf_categories/payments.tsx +++ /dev/null @@ -1,138 +0,0 @@ -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; -import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple'; - -import { dayjs } from '@/lib/dayjs'; -import type { ColumnDef } from '@/components/core/data-table'; -import { DataTable } from '@/components/core/data-table'; - -export interface Payment { - currency: string; - amount: number; - invoiceId: string; - status: 'pending' | 'completed' | 'canceled' | 'refunded'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)} - - ), - name: 'Amount', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - pending: { label: 'Pending', color: 'warning' }, - completed: { label: 'Completed', color: 'success' }, - canceled: { label: 'Canceled', color: 'error' }, - refunded: { label: 'Refunded', color: 'error' }, - } as const; - const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; - - return ; - }, - name: 'Status', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - return {row.invoiceId}; - }, - name: 'Invoice ID', - width: '150px', - }, - { - formatter: (row): React.JSX.Element => ( - - {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} - - ), - name: 'Date', - align: 'right', - }, -] satisfies ColumnDef[]; - -export interface PaymentsProps { - ordersValue: number; - payments: Payment[]; - refundsValue: number; - totalOrders: number; -} - -export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { - return ( - - }> - Create Payment - - } - avatar={ - - - - } - title="Payments" - /> - - - - } - spacing={3} - sx={{ justifyContent: 'space-between', p: 2 }} - > -
- - Total orders - - {new Intl.NumberFormat('en-US').format(totalOrders)} -
-
- - Orders value - - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} - -
-
- - Refunds - - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} - -
-
-
- - - columns={columns} rows={payments} /> - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/cf_categories/shipping-address.tsx b/002_source/cms/src/components/dashboard/cf_categories/shipping-address.tsx deleted file mode 100644 index 8793e5c..0000000 --- a/002_source/cms/src/components/dashboard/cf_categories/shipping-address.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from 'react'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; - -export interface Address { - id: string; - country: string; - state: string; - city: string; - zipCode: string; - street: string; - primary?: boolean; -} - -export interface ShippingAddressProps { - address: Address; -} - -export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement { - return ( - - - - - {address.street}, -
- {address.city}, {address.state}, {address.country}, -
- {address.zipCode} -
- - {address.primary ? : } - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/cf_questions/customer-create-form.tsx b/002_source/cms/src/components/dashboard/cf_questions/customer-create-form.tsx deleted file mode 100644 index 7be8fc8..0000000 --- a/002_source/cms/src/components/dashboard/cf_questions/customer-create-form.tsx +++ /dev/null @@ -1,398 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useRouter } from 'next/navigation'; -import { zodResolver } from '@hookform/resolvers/zod'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardActions from '@mui/material/CardActions'; -import CardContent from '@mui/material/CardContent'; -import Checkbox from '@mui/material/Checkbox'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import FormHelperText from '@mui/material/FormHelperText'; -import InputLabel from '@mui/material/InputLabel'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Select from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import Grid from '@mui/material/Unstable_Grid2'; -import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; -import { Controller, useForm } from 'react-hook-form'; -import { z as zod } from 'zod'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { Option } from '@/components/core/option'; -import { toast } from '@/components/core/toaster'; - -function fileToBase64(file: Blob): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - resolve(reader.result as string); - }; - reader.onerror = () => { - reject(new Error('Error converting file to base64')); - }; - }); -} - -const schema = zod.object({ - avatar: zod.string().optional(), - name: zod.string().min(1, 'Name is required').max(255), - email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255), - phone: zod.string().min(1, 'Phone is required').max(15), - company: zod.string().max(255), - billingAddress: zod.object({ - country: zod.string().min(1, 'Country is required').max(255), - state: zod.string().min(1, 'State is required').max(255), - city: zod.string().min(1, 'City is required').max(255), - zipCode: zod.string().min(1, 'Zip code is required').max(255), - line1: zod.string().min(1, 'Street line 1 is required').max(255), - line2: zod.string().max(255).optional(), - }), - taxId: zod.string().max(255).optional(), - timezone: zod.string().min(1, 'Timezone is required').max(255), - language: zod.string().min(1, 'Language is required').max(255), - currency: zod.string().min(1, 'Currency is required').max(255), -}); - -type Values = zod.infer; - -const defaultValues = { - avatar: '', - name: '', - email: '', - phone: '', - company: '', - billingAddress: { country: '', state: '', city: '', zipCode: '', line1: '', line2: '' }, - taxId: '', - timezone: 'new_york', - language: 'en', - currency: 'USD', -} satisfies Values; - -export function CustomerCreateForm(): React.JSX.Element { - const router = useRouter(); - - const { - control, - handleSubmit, - formState: { errors }, - setValue, - watch, - } = useForm({ defaultValues, resolver: zodResolver(schema) }); - - const onSubmit = React.useCallback( - async (_: Values): Promise => { - try { - // Make API request - toast.success('Customer updated'); - router.push(paths.dashboard.customers.details('1')); - } catch (err) { - logger.error(err); - toast.error('Something went wrong!'); - } - }, - [router] - ); - - const avatarInputRef = React.useRef(null); - const avatar = watch('avatar'); - - const handleAvatarChange = React.useCallback( - async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - - if (file) { - const url = await fileToBase64(file); - setValue('avatar', url); - } - }, - [setValue] - ); - - return ( -
- - - } spacing={4}> - - Account information - - - - - - - - - - Avatar - Min 400x400px, PNG or JPEG - - - - - - - ( - - Name - - {errors.name ? {errors.name.message} : null} - - )} - /> - - - ( - - Email address - - {errors.email ? {errors.email.message} : null} - - )} - /> - - - ( - - Phone number - - {errors.phone ? {errors.phone.message} : null} - - )} - /> - - - ( - - Company - - {errors.company ? {errors.company.message} : null} - - )} - /> - - - - - Billing information - - - ( - - Country - - {errors.billingAddress?.country ? ( - {errors.billingAddress?.country?.message} - ) : null} - - )} - /> - - - ( - - State - - {errors.billingAddress?.state ? ( - {errors.billingAddress?.state?.message} - ) : null} - - )} - /> - - - ( - - City - - {errors.billingAddress?.city ? ( - {errors.billingAddress?.city?.message} - ) : null} - - )} - /> - - - ( - - Zip code - - {errors.billingAddress?.zipCode ? ( - {errors.billingAddress?.zipCode?.message} - ) : null} - - )} - /> - - - ( - - Address - - {errors.billingAddress?.line1 ? ( - {errors.billingAddress?.line1?.message} - ) : null} - - )} - /> - - - ( - - Tax ID - - {errors.taxId ? {errors.taxId.message} : null} - - )} - /> - - - - - Shipping information - } label="Same as billing address" /> - - - Additional information - - - ( - - Timezone - - {errors.timezone ? {errors.timezone.message} : null} - - )} - /> - - - ( - - Language - - {errors.language ? {errors.language.message} : null} - - )} - /> - - - ( - - Currency - - {errors.currency ? {errors.currency.message} : null} - - )} - /> - - - - - - - - - - -
- ); -} diff --git a/002_source/cms/src/components/dashboard/cf_questions/customers-filters.tsx b/002_source/cms/src/components/dashboard/cf_questions/customers-filters.tsx deleted file mode 100644 index 1567e3b..0000000 --- a/002_source/cms/src/components/dashboard/cf_questions/customers-filters.tsx +++ /dev/null @@ -1,241 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import Button from '@mui/material/Button'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Select from '@mui/material/Select'; -import type { SelectChangeEvent } from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Tab from '@mui/material/Tab'; -import Tabs from '@mui/material/Tabs'; -import Typography from '@mui/material/Typography'; - -import { paths } from '@/paths'; -import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; -import { Option } from '@/components/core/option'; - -import { useCustomersSelection } from './customers-selection-context'; - -// The tabs should be generated using API data. -const tabs = [ - { label: 'All', value: '', count: 5 }, - { label: 'Active', value: 'active', count: 3 }, - { label: 'Pending', value: 'pending', count: 1 }, - { label: 'Blocked', value: 'blocked', count: 1 }, -] as const; - -export interface Filters { - email?: string; - phone?: string; - status?: string; -} - -export type SortDir = 'asc' | 'desc'; - -export interface CustomersFiltersProps { - filters?: Filters; - sortDir?: SortDir; -} - -export function CustomersFilters({ filters = {}, sortDir = 'desc' }: CustomersFiltersProps): React.JSX.Element { - const { email, phone, status } = filters; - - const router = useRouter(); - - const selection = useCustomersSelection(); - - const updateSearchParams = React.useCallback( - (newFilters: Filters, newSortDir: SortDir): void => { - const searchParams = new URLSearchParams(); - - if (newSortDir === 'asc') { - searchParams.set('sortDir', newSortDir); - } - - if (newFilters.status) { - searchParams.set('status', newFilters.status); - } - - if (newFilters.email) { - searchParams.set('email', newFilters.email); - } - - if (newFilters.phone) { - searchParams.set('phone', newFilters.phone); - } - - router.push(`${paths.dashboard.customers.list}?${searchParams.toString()}`); - }, - [router] - ); - - const handleClearFilters = React.useCallback(() => { - updateSearchParams({}, sortDir); - }, [updateSearchParams, sortDir]); - - const handleStatusChange = React.useCallback( - (_: React.SyntheticEvent, value: string) => { - updateSearchParams({ ...filters, status: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleEmailChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, email: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handlePhoneChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, phone: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleSortChange = React.useCallback( - (event: SelectChangeEvent) => { - updateSearchParams(filters, event.target.value as SortDir); - }, - [updateSearchParams, filters] - ); - - const hasFilters = status || email || phone; - - return ( -
- - {tabs.map((tab) => ( - } - iconPosition="end" - key={tab.value} - label={tab.label} - sx={{ minHeight: 'auto' }} - tabIndex={0} - value={tab.value} - /> - ))} - - - - - { - handleEmailChange(value as string); - }} - onFilterDelete={() => { - handleEmailChange(); - }} - popover={} - value={email} - /> - { - handlePhoneChange(value as string); - }} - onFilterDelete={() => { - handlePhoneChange(); - }} - popover={} - value={phone} - /> - {hasFilters ? : null} - - {selection.selectedAny ? ( - - - {selection.selected.size} selected - - - - ) : null} - - -
- ); -} - -function EmailFilterPopover(): React.JSX.Element { - 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 PhoneFilterPopover(): React.JSX.Element { - 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} - /> - - - - ); -} diff --git a/002_source/cms/src/components/dashboard/cf_questions/customers-pagination.tsx b/002_source/cms/src/components/dashboard/cf_questions/customers-pagination.tsx deleted file mode 100644 index ab01272..0000000 --- a/002_source/cms/src/components/dashboard/cf_questions/customers-pagination.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import * as React from 'react'; -import TablePagination from '@mui/material/TablePagination'; - -function noop(): void { - return undefined; -} - -interface CustomersPaginationProps { - count: number; - page: number; -} - -export function CustomersPagination({ count, page }: CustomersPaginationProps): React.JSX.Element { - // You should implement the pagination using a similar logic as the filters. - // Note that when page change, you should keep the filter search params. - - return ( - - ); -} diff --git a/002_source/cms/src/components/dashboard/cf_questions/customers-selection-context.tsx b/002_source/cms/src/components/dashboard/cf_questions/customers-selection-context.tsx deleted file mode 100644 index 023dbc0..0000000 --- a/002_source/cms/src/components/dashboard/cf_questions/customers-selection-context.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import * as React from 'react'; - -import { useSelection } from '@/hooks/use-selection'; -import type { Selection } from '@/hooks/use-selection'; - -import type { Customer } from './customers-table'; - -function noop(): void { - return undefined; -} - -export interface CustomersSelectionContextValue extends Selection {} - -export const CustomersSelectionContext = React.createContext({ - deselectAll: noop, - deselectOne: noop, - selectAll: noop, - selectOne: noop, - selected: new Set(), - selectedAny: false, - selectedAll: false, -}); - -interface CustomersSelectionProviderProps { - children: React.ReactNode; - customers: Customer[]; -} - -export function CustomersSelectionProvider({ - children, - customers = [], -}: CustomersSelectionProviderProps): React.JSX.Element { - const customerIds = React.useMemo(() => customers.map((customer) => customer.id), [customers]); - const selection = useSelection(customerIds); - - return {children}; -} - -export function useCustomersSelection(): CustomersSelectionContextValue { - return React.useContext(CustomersSelectionContext); -} diff --git a/002_source/cms/src/components/dashboard/cf_questions/customers-table.tsx b/002_source/cms/src/components/dashboard/cf_questions/customers-table.tsx deleted file mode 100644 index bf9b01a..0000000 --- a/002_source/cms/src/components/dashboard/cf_questions/customers-table.tsx +++ /dev/null @@ -1,139 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Chip from '@mui/material/Chip'; -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 { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; -import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; -import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; - -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 { useCustomersSelection } from './customers-selection-context'; - -export interface Customer { - id: string; - name: string; - avatar?: string; - email: string; - phone?: string; - quota: number; - status: 'pending' | 'active' | 'blocked'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {' '} -
- - {row.name} - - - {row.email} - -
-
- ), - name: 'Name', - width: '250px', - }, - { - formatter: (row): React.JSX.Element => ( - - - - {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} - - - ), - name: 'Quota', - width: '250px', - }, - { field: 'phone', name: 'Phone number', width: '150px' }, - { - formatter(row) { - return dayjs(row.createdAt).format('MMM D, YYYY h:mm A'); - }, - name: 'Created at', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - active: { label: 'Active', icon: }, - blocked: { label: 'Blocked', icon: }, - pending: { label: 'Pending', icon: }, - } as const; - const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; - - return ; - }, - name: 'Status', - width: '150px', - }, - { - formatter: (): React.JSX.Element => ( - - - - ), - name: 'Actions', - hideName: true, - width: '100px', - align: 'right', - }, -] satisfies ColumnDef[]; - -export interface CustomersTableProps { - rows: Customer[]; -} - -export function CustomersTable({ rows }: CustomersTableProps): React.JSX.Element { - const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCustomersSelection(); - - return ( - - - columns={columns} - onDeselectAll={deselectAll} - onDeselectOne={(_, row) => { - deselectOne(row.id); - }} - onSelectAll={selectAll} - onSelectOne={(_, row) => { - selectOne(row.id); - }} - rows={rows} - selectable - selected={selected} - /> - {!rows.length ? ( - - - No customers found - - - ) : null} - - ); -} diff --git a/002_source/cms/src/components/dashboard/cf_questions/helloworld.tsx b/002_source/cms/src/components/dashboard/cf_questions/helloworld.tsx deleted file mode 100644 index 3989cb1..0000000 --- a/002_source/cms/src/components/dashboard/cf_questions/helloworld.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const helloworld = 'helloworld'; - -export { helloworld }; diff --git a/002_source/cms/src/components/dashboard/cf_questions/notifications.tsx b/002_source/cms/src/components/dashboard/cf_questions/notifications.tsx deleted file mode 100644 index a6c16bd..0000000 --- a/002_source/cms/src/components/dashboard/cf_questions/notifications.tsx +++ /dev/null @@ -1,101 +0,0 @@ -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import Chip from '@mui/material/Chip'; -import Select from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; - -import { dayjs } from '@/lib/dayjs'; -import { DataTable } from '@/components/core/data-table'; -import type { ColumnDef } from '@/components/core/data-table'; -import { Option } from '@/components/core/option'; - -export interface Notification { - id: string; - type: string; - status: 'delivered' | 'pending' | 'failed'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {row.type} - - ), - name: 'Type', - width: '300px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - delivered: { label: 'Delivered', color: 'success' }, - pending: { label: 'Pending', color: 'warning' }, - failed: { label: 'Failed', color: 'error' }, - } as const; - const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; - - return ; - }, - name: 'Status', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => ( - - {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} - - ), - name: 'Date', - align: 'right', - }, -] satisfies ColumnDef[]; - -export interface NotificationsProps { - notifications: Notification[]; -} - -export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { - return ( - - - - - } - title="Notifications" - /> - - - - -
- -
-
- - - columns={columns} rows={notifications} /> - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/cf_questions/payments.tsx b/002_source/cms/src/components/dashboard/cf_questions/payments.tsx deleted file mode 100644 index 0420d32..0000000 --- a/002_source/cms/src/components/dashboard/cf_questions/payments.tsx +++ /dev/null @@ -1,138 +0,0 @@ -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; -import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple'; - -import { dayjs } from '@/lib/dayjs'; -import type { ColumnDef } from '@/components/core/data-table'; -import { DataTable } from '@/components/core/data-table'; - -export interface Payment { - currency: string; - amount: number; - invoiceId: string; - status: 'pending' | 'completed' | 'canceled' | 'refunded'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)} - - ), - name: 'Amount', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - pending: { label: 'Pending', color: 'warning' }, - completed: { label: 'Completed', color: 'success' }, - canceled: { label: 'Canceled', color: 'error' }, - refunded: { label: 'Refunded', color: 'error' }, - } as const; - const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; - - return ; - }, - name: 'Status', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - return {row.invoiceId}; - }, - name: 'Invoice ID', - width: '150px', - }, - { - formatter: (row): React.JSX.Element => ( - - {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} - - ), - name: 'Date', - align: 'right', - }, -] satisfies ColumnDef[]; - -export interface PaymentsProps { - ordersValue: number; - payments: Payment[]; - refundsValue: number; - totalOrders: number; -} - -export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { - return ( - - }> - Create Payment - - } - avatar={ - - - - } - title="Payments" - /> - - - - } - spacing={3} - sx={{ justifyContent: 'space-between', p: 2 }} - > -
- - Total orders - - {new Intl.NumberFormat('en-US').format(totalOrders)} -
-
- - Orders value - - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} - -
-
- - Refunds - - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} - -
-
-
- - - columns={columns} rows={payments} /> - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/cf_questions/shipping-address.tsx b/002_source/cms/src/components/dashboard/cf_questions/shipping-address.tsx deleted file mode 100644 index 8793e5c..0000000 --- a/002_source/cms/src/components/dashboard/cf_questions/shipping-address.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from 'react'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; - -export interface Address { - id: string; - country: string; - state: string; - city: string; - zipCode: string; - street: string; - primary?: boolean; -} - -export interface ShippingAddressProps { - address: Address; -} - -export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement { - return ( - - - - - {address.street}, -
- {address.city}, {address.state}, {address.country}, -
- {address.zipCode} -
- - {address.primary ? : } - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/cr/_repomix.md b/002_source/cms/src/components/dashboard/cr/_repomix.md new file mode 100644 index 0000000..12d24af --- /dev/null +++ b/002_source/cms/src/components/dashboard/cr/_repomix.md @@ -0,0 +1,9439 @@ +This file is a merged representation of the entire codebase, combined into a single document by Repomix. + +# File Summary + +## Purpose +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. + +## File Format +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Multiple file entries, each consisting of: + a. A header with the file path (## File: path/to/file) + b. The full contents of the file in a code block + +## Usage Guidelines +- 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. + +## Notes +- 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) + +## Additional Info + +# Directory Structure +``` +categories/ + _constants.ts + _SUMMARY.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 + 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 +repomix-output.xml +``` + +# Files + +## File: categories/_constants.ts +````typescript +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, +}; +```` + +## File: categories/_SUMMARY.md +````markdown +# 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 +``` +```` + +## File: categories/confirm-delete-modal.tsx +````typescript +'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')} + + + + + + + + +
+ ); +} +```` + +## File: categories/cr-categories-filters.tsx +````typescript +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +// import { COL_LESSON_CATEGORIES } from '@/constants'; + +// RULES: Quiz +import GetAllCount from '@/db/QuizMFCategories/GetAllCount'; +import GetHiddenCount from '@/db/QuizMFCategories/GetHiddenCount'; +import GetVisibleCount from '@/db/QuizMFCategories/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 { useCrCategoriesSelection } from './cr-categories-selection-context'; +import type { 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} + /> + + + + ); +} +```` + +## File: categories/cr-categories-pagination.tsx +````typescript +'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 ( + + ); +} +```` + +## File: categories/cr-categories-selection-context.tsx +````typescript +'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); +} +```` + +## File: categories/cr-categories-table.tsx +````typescript +'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} + + ); +} +```` + +## File: categories/cr-category-create-form.tsx +````typescript +'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)}
+
+
+ ); +} +```` + +## File: categories/cr-category-edit-form.tsx +````typescript +'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')} + + + +
+ ); +} +```` + +## File: categories/notifications.tsx +````typescript +'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} /> + + +
+
+
+ ); +} +```` + +## File: categories/payments.tsx +````typescript +'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} /> + + +
+
+
+ ); +} +```` + +## File: categories/shipping-address.tsx +````typescript +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 ? : } + + +
+
+
+ ); +} +```` + +## File: categories/type.d.ts +````typescript +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; +} +```` + +## File: questions/_constants.ts +````typescript +import { dayjs } from '@/lib/dayjs'; + +import { CreateFormProps, CrQuestion } from './type'; + +export const defaultCrQuestion: CrQuestion = { + 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: CrQuestion = { + ...defaultCrQuestion, + isEmpty: true, +}; +```` + +## File: questions/confirm-delete-modal.tsx +````typescript +'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')} + + + + + + + + +
+ ); +} +```` + +## File: questions/cr-question-create-form.tsx +````typescript +'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 CrQuestionCreateForm(): 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)}
+
+
+ ); +} +```` + +## File: questions/cr-question-edit-form.tsx +````typescript +'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 CrQuestionEditForm(): 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')} + + + +
+ ); +} +```` + +## File: questions/cr-questions-filters.tsx +````typescript +'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 { useCrQuestionsSelection } from './cr-questions-selection-context'; +import { CrQuestion } 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: CrQuestion[]; +} + +export function CrQuestionsFilters({ + 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 = useCrQuestionsSelection(); + + function getVisible(): number { + return fullData.reduce((count, item: CrQuestion) => { + return item.visible === 'visible' ? count + 1 : count; + }, 0); + } + + function getHidden(): number { + return fullData.reduce((count, item: CrQuestion) => { + 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} + /> + + + + ); +} +```` + +## File: questions/cr-questions-pagination.tsx +````typescript +'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 CrQuestionsPagination({ + 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 ( + + ); +} +```` + +## File: questions/cr-questions-selection-context.tsx +````typescript +'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 { CrQuestion } from './type'; + +function noop(): void { + return undefined; +} + +export interface CrQuestionsSelectionContextValue extends Selection {} + +export const CrQuestionsSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface CrQuestionsSelectionProviderProps { + children: React.ReactNode; + lessonQuestions: CrQuestion[]; +} + +export function CrQuestionsSelectionProvider({ + children, + lessonQuestions = [], +}: CrQuestionsSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => lessonQuestions.map((customer) => customer.id), [lessonQuestions]); + const selection = useSelection(customerIds); + + return ( + {children} + ); +} + +export function useCrQuestionsSelection(): CrQuestionsSelectionContextValue { + return React.useContext(CrQuestionsSelectionContext); +} +```` + +## File: questions/cr-questions-table.tsx +````typescript +'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 { useCrQuestionsSelection } from './cr-questions-selection-context'; +import type { CrQuestion } 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: CrQuestion[]; + reloadRows: () => void; +} + +export function CrQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps): React.JSX.Element { + const { t } = useTranslation(['lp_categories']); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCrQuestionsSelection(); + + 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} + + ); +} +```` + +## File: questions/notifications.tsx +````typescript +'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} /> + + +
+
+
+ ); +} +```` + +## File: questions/payments.tsx +````typescript +'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} /> + + +
+
+
+ ); +} +```` + +## File: questions/shipping-address.tsx +````typescript +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 ? : } + + +
+
+
+ ); +} +```` + +## File: questions/type.d.ts +````typescript +export interface CrQuestion { + 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; +} +```` + +## File: repomix-output.xml +````xml +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 + _SUMMARY.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 + 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, +}; + + + +# 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 +``` + + + +'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/QuizMFCategories/GetAllCount'; +import GetHiddenCount from '@/db/QuizMFCategories/GetHiddenCount'; +import GetVisibleCount from '@/db/QuizMFCategories/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 { useCrCategoriesSelection } from './cr-categories-selection-context'; +import type { 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, CrQuestion } from './type'; + +export const defaultCrQuestion: CrQuestion = { + 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: CrQuestion = { + ...defaultCrQuestion, + isEmpty: true, +}; + + + +'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 CrQuestionCreateForm(): 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 CrQuestionEditForm(): 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 { useCrQuestionsSelection } from './cr-questions-selection-context'; +import { CrQuestion } 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: CrQuestion[]; +} + +export function CrQuestionsFilters({ + 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 = useCrQuestionsSelection(); + + function getVisible(): number { + return fullData.reduce((count, item: CrQuestion) => { + return item.visible === 'visible' ? count + 1 : count; + }, 0); + } + + function getHidden(): number { + return fullData.reduce((count, item: CrQuestion) => { + 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 CrQuestionsPagination({ + 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 { CrQuestion } from './type'; + +function noop(): void { + return undefined; +} + +export interface CrQuestionsSelectionContextValue extends Selection {} + +export const CrQuestionsSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface CrQuestionsSelectionProviderProps { + children: React.ReactNode; + lessonQuestions: CrQuestion[]; +} + +export function CrQuestionsSelectionProvider({ + children, + lessonQuestions = [], +}: CrQuestionsSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => lessonQuestions.map((customer) => customer.id), [lessonQuestions]); + const selection = useSelection(customerIds); + + return ( + {children} + ); +} + +export function useCrQuestionsSelection(): CrQuestionsSelectionContextValue { + return React.useContext(CrQuestionsSelectionContext); +} + + + +'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 { useCrQuestionsSelection } from './cr-questions-selection-context'; +import type { CrQuestion } 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: CrQuestion[]; + reloadRows: () => void; +} + +export function CrQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps): React.JSX.Element { + const { t } = useTranslation(['lp_categories']); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCrQuestionsSelection(); + + 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 CrQuestion { + 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/cr/repomix-output.xml b/002_source/cms/src/components/dashboard/cr/repomix-output.xml index 0a4a9a7..5e7bf6b 100644 --- a/002_source/cms/src/components/dashboard/cr/repomix-output.xml +++ b/002_source/cms/src/components/dashboard/cr/repomix-output.xml @@ -45,7 +45,7 @@ The content is organized as follows: categories/ _constants.ts - _PROMPT.MD + _SUMMARY.md confirm-delete-modal.tsx cr-categories-filters.tsx cr-categories-pagination.tsx @@ -59,7 +59,6 @@ categories/ type.d.ts questions/ _constants.ts - _PROMPT.MD confirm-delete-modal.tsx cr-question-create-form.tsx cr-question-edit-form.tsx @@ -123,8 +122,101 @@ export const emptyCrCategory: CrCategory = { }; - -please review and update translations + +# 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 +``` @@ -265,10 +357,9 @@ 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 GetAllCount from '@/db/QuizMFCategories/GetAllCount'; +import GetHiddenCount from '@/db/QuizMFCategories/GetHiddenCount'; +import GetVisibleCount from '@/db/QuizMFCategories/GetVisibleCount'; // import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; @@ -288,7 +379,7 @@ import { FilterButton, FilterPopover, useFilterContext } from '@/components/core import { Option } from '@/components/core/option'; import { useCrCategoriesSelection } from './cr-categories-selection-context'; -import { CrCategory } from './type'; +import type { CrCategory } from './type'; export interface Filters { email?: string; @@ -2355,9 +2446,9 @@ export interface Helloworld { 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', @@ -2393,36 +2484,12 @@ export const defaultLpQuestion: LpQuestion = { // imageUrl: '', // }; -export const emptyLpQuestion: LpQuestion = { - ...defaultLpQuestion, +export const emptyLpQuestion: CrQuestion = { + ...defaultCrQuestion, 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'; @@ -2620,7 +2687,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']); @@ -3064,7 +3131,7 @@ const defaultValues = { description: '', } satisfies Values; -export function LpQuestionEditForm(): React.JSX.Element { +export function CrQuestionEditForm(): React.JSX.Element { const router = useRouter(); const { t } = useTranslation(['lp_categories']); @@ -3502,8 +3569,8 @@ 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'; +import { useCrQuestionsSelection } from './cr-questions-selection-context'; +import { CrQuestion } from './type'; export interface Filters { email?: string; @@ -3519,10 +3586,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, @@ -3536,16 +3603,16 @@ export function LpQuestionsFilters({ const router = useRouter(); - const selection = useLpQuestionsSelection(); + const selection = useCrQuestionsSelection(); 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); } @@ -3954,7 +4021,7 @@ interface LessonQuestionsPaginationProps { rowsPerPage: number; } -export function LpQuestionsPagination({ +export function CrQuestionsPagination({ count, page, // @@ -3982,6 +4049,7 @@ export function LpQuestionsPagination({ page={page} rowsPerPage={rowsPerPage} rowsPerPageOptions={[5, 10, 25]} + // /> ); } @@ -3996,15 +4064,15 @@ 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; } -export interface LpQuestionsSelectionContextValue extends Selection {} +export interface CrQuestionsSelectionContextValue extends Selection {} -export const LpQuestionsSelectionContext = React.createContext({ +export const CrQuestionsSelectionContext = React.createContext({ deselectAll: noop, deselectOne: noop, selectAll: noop, @@ -4014,25 +4082,25 @@ export const LpQuestionsSelectionContext = React.createContext lessonQuestions.map((customer) => customer.id), [lessonQuestions]); const selection = useSelection(customerIds); return ( - {children} + {children} ); } -export function useLpQuestionsSelection(): LpQuestionsSelectionContextValue { - return React.useContext(LpQuestionsSelectionContext); +export function useCrQuestionsSelection(): CrQuestionsSelectionContextValue { + return React.useContext(CrQuestionsSelectionContext); } @@ -4064,10 +4132,10 @@ import { DataTable } from '@/components/core/data-table'; import type { ColumnDef } from '@/components/core/data-table'; import ConfirmDeleteModal from './confirm-delete-modal'; -import { useLpQuestionsSelection } from './cr-questions-selection-context'; -import type { LpQuestion } from './type'; +import { useCrQuestionsSelection } from './cr-questions-selection-context'; +import type { CrQuestion } from './type'; -function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { +function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { return [ { formatter: (row): React.JSX.Element => ( @@ -4079,7 +4147,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef @@ -4202,7 +4270,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef @@ -4226,13 +4294,13 @@ 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(); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCrQuestionsSelection(); const [idToDelete, setIdToDelete] = React.useState(''); const [open, setOpen] = React.useState(false); @@ -4250,7 +4318,7 @@ export function LpQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps reloadRows={reloadRows} setOpen={setOpen} /> - + columns={columns(handleDeleteClick)} onDeselectAll={deselectAll} onDeselectOne={(_, row) => { @@ -4576,7 +4644,7 @@ export function ShippingAddress({ address }: ShippingAddressProps): React.ReactE -export interface LpQuestion { +export interface CrQuestion { isEmpty?: boolean; // id: string; diff --git a/002_source/cms/src/components/dashboard/customer/_GUIDELINES.md b/002_source/cms/src/components/dashboard/customer/_GUIDELINES.md new file mode 100644 index 0000000..709449f --- /dev/null +++ b/002_source/cms/src/components/dashboard/customer/_GUIDELINES.md @@ -0,0 +1,25 @@ +# GUIDELINES & KEY COMPONENTS + +- `_constants.ts` contains the constant for + + - default value (defaultValue) + - empty value (emptyValue) + +- `customers-table.tsx` + +- `confirm-delete-modal.tsx` - delete modal component when click delete button on list + + - `customers-filters.tsx` + - `customers-pagination.tsx` + - `email-filter-popover.tsx` + - `phone-filter-popover.tsx` + - `customers-selection-context.tsx` + +- `customer-create-form.tsx` - form to create a new customer +- `customer-edit-form.tsx` - form to edit a existing customer + +- `type.d.tsx` - contains type definition + +- `notifications.tsx` - constants used for demonstration +- `payments.tsx` - constants used for demonstration +- `shipping-address.tsx` - constants used for demonstration diff --git a/002_source/cms/src/components/dashboard/customer/_NOTES.md b/002_source/cms/src/components/dashboard/customer/_NOTES.md deleted file mode 100644 index e183566..0000000 --- a/002_source/cms/src/components/dashboard/customer/_NOTES.md +++ /dev/null @@ -1,9 +0,0 @@ -# task - -Create a customer edit form - -## steps - -- read other `tsx` files in same directory, -- draft `customer-edit-form.tsx`. -- the `customer-edit-form.tsx` is already there with content, you can modify it freely thanks. diff --git a/002_source/cms/src/components/dashboard/customer/_constants.ts b/002_source/cms/src/components/dashboard/customer/_constants.ts index 2e8381b..503e5e3 100644 --- a/002_source/cms/src/components/dashboard/customer/_constants.ts +++ b/002_source/cms/src/components/dashboard/customer/_constants.ts @@ -1,3 +1,7 @@ +// RULES: +// default variable value for customer +// empty valur for customer + import { dayjs } from '@/lib/dayjs'; import type { Customer } from './type.d'; diff --git a/002_source/cms/src/components/dashboard/customer/customers-filters.tsx b/002_source/cms/src/components/dashboard/customer/customers-filters.tsx index c847641..56a7e4f 100644 --- a/002_source/cms/src/components/dashboard/customer/customers-filters.tsx +++ b/002_source/cms/src/components/dashboard/customer/customers-filters.tsx @@ -47,17 +47,17 @@ export function CustomersFilters({ const selection = useCustomersSelection(); - function getVisible(): number { - return fullData.reduce((count, item: CrQuestion) => { - return item.visible === 'visible' ? count + 1 : count; - }, 0); - } + // function getVisible(): number { + // return fullData.reduce((count, item: CrQuestion) => { + // return item.visible === 'visible' ? count + 1 : count; + // }, 0); + // } - function getHidden(): number { - return fullData.reduce((count, item: CrQuestion) => { - return item.visible === 'hidden' ? count + 1 : count; - }, 0); - } + // function getHidden(): number { + // return fullData.reduce((count, item: CrQuestion) => { + // return item.visible === 'hidden' ? count + 1 : count; + // }, 0); + // } // The tabs should be generated using API data. const tabs = [ diff --git a/002_source/cms/src/components/dashboard/customer/customers-table.tsx b/002_source/cms/src/components/dashboard/customer/customers-table.tsx index d22e1c9..f064433 100644 --- a/002_source/cms/src/components/dashboard/customer/customers-table.tsx +++ b/002_source/cms/src/components/dashboard/customer/customers-table.tsx @@ -149,7 +149,8 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef { handleDeleteClick(row.id); }} diff --git a/002_source/cms/src/components/dashboard/lp/_repomix.md b/002_source/cms/src/components/dashboard/lp/_repomix.md new file mode 100644 index 0000000..b5da18b --- /dev/null +++ b/002_source/cms/src/components/dashboard/lp/_repomix.md @@ -0,0 +1,13178 @@ +This file is a merged representation of the entire codebase, combined into a single document by Repomix. + +# File Summary + +## Purpose +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. + +## File Format +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Multiple file entries, each consisting of: + a. A header with the file path (## File: path/to/file) + b. The full contents of the file in a code block + +## Usage Guidelines +- 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. + +## Notes +- 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) + +## Additional Info + +# Directory Structure +``` +categories/ + _constants.ts + _PROMPT.MD + _SUMMARY.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 + _SUMMARY.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 +repomix-output.xml +``` + +# Files + +## File: categories/_constants.ts +````typescript +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, +}; +```` + +## File: categories/_PROMPT.MD +````markdown +please review and add translations, e.g. `{t('[word]')}` +```` + +## File: categories/_SUMMARY.md +````markdown +# 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 +``` +```` + +## File: categories/confirm-delete-modal.tsx +````typescript +'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')} + + + + + + + + +
+ ); +} +```` + +## File: categories/lp-categories-filters.tsx +````typescript +'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} + /> + + + + ); +} +```` + +## File: categories/lp-categories-pagination.tsx +````typescript +'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 ( + + ); +} +```` + +## File: categories/lp-categories-selection-context.tsx +````typescript +'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); +} +```` + +## File: categories/lp-categories-table.tsx +````typescript +'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} + + ); +} +```` + +## File: categories/lp-category-create-form.tsx +````typescript +'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)}
+
+
+ ); +} +```` + +## File: categories/lp-category-edit-form.tsx +````typescript +'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')} + + + +
+ ); +} +```` + +## File: categories/mf-categories-filters.tsx +````typescript +'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} + /> + + + + ); +} +```` + +## File: categories/mf-categories-pagination.tsx +````typescript +'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 ( + + ); +} +```` + +## File: categories/mf-categories-selection-context.tsx +````typescript +'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); +} +```` + +## File: categories/mf-categories-table.tsx +````typescript +'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} + + ); +} +```` + +## File: categories/mf-category-create-form.tsx +````typescript +'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)}
+
+
+ ); +} +```` + +## File: categories/mf-category-edit-form.tsx +````typescript +'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')} + + + +
+ ); +} +```` + +## File: categories/notifications.tsx +````typescript +'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} /> + + +
+
+
+ ); +} +```` + +## File: categories/payments.tsx +````typescript +'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} /> + + +
+
+
+ ); +} +```` + +## File: categories/shipping-address.tsx +````typescript +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 ? : } + + +
+
+
+ ); +} +```` + +## File: categories/type.d.ts +````typescript +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; +} +```` + +## File: questions/_constants.ts +````typescript +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, +}; +```` + +## File: questions/_PROMPT.MD +````markdown +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 +```` + +## File: questions/_SUMMARY.md +````markdown +# 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 +``` +```` + +## File: questions/confirm-delete-modal.tsx +````typescript +'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')} + + + + + + + + +
+ ); +} +```` + +## File: questions/lp-question-create-form.tsx +````typescript +'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)}
+
+
+ ); +} +```` + +## File: questions/lp-question-edit-form.tsx +````typescript +'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')} + + + +
+ ); +} +```` + +## File: questions/lp-questions-filters.tsx +````typescript +'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} + /> + + + + ); +} +```` + +## File: questions/lp-questions-pagination.tsx +````typescript +'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 ( + + ); +} +```` + +## File: questions/lp-questions-selection-context.tsx +````typescript +'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); +} +```` + +## File: questions/lp-questions-table.tsx +````typescript +'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} + + ); +} +```` + +## File: questions/notifications.tsx +````typescript +'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} /> + + +
+
+
+ ); +} +```` + +## File: questions/payments.tsx +````typescript +'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} /> + + +
+
+
+ ); +} +```` + +## File: questions/shipping-address.tsx +````typescript +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 ? : } + + +
+
+
+ ); +} +```` + +## File: questions/type.d.ts +````typescript +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; +} +```` + +## File: repomix-output.xml +````xml +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 + _SUMMARY.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 + _SUMMARY.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]')}` + + + +# 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 +``` + + + +'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 + + + +# 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 +``` + + + +'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/lp/repomix-output.xml b/002_source/cms/src/components/dashboard/lp/repomix-output.xml index 9c97ce2..d23f0cc 100644 --- a/002_source/cms/src/components/dashboard/lp/repomix-output.xml +++ b/002_source/cms/src/components/dashboard/lp/repomix-output.xml @@ -46,6 +46,7 @@ The content is organized as follows: categories/ _constants.ts _PROMPT.MD + _SUMMARY.md confirm-delete-modal.tsx lp-categories-filters.tsx lp-categories-pagination.tsx @@ -66,6 +67,7 @@ categories/ questions/ _constants.ts _PROMPT.MD + _SUMMARY.md confirm-delete-modal.tsx lp-question-create-form.tsx lp-question-edit-form.tsx @@ -133,6 +135,120 @@ export const emptyLpCategory: LpCategory = { please review and add translations, e.g. `{t('[word]')}`
+ +# 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 +``` + + 'use client'; @@ -4151,6 +4267,95 @@ keep current folder structure is important thanks + +# 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 +``` + + 'use client'; diff --git a/002_source/cms/src/components/dashboard/mf/_repomix.md b/002_source/cms/src/components/dashboard/mf/_repomix.md new file mode 100644 index 0000000..3c4f193 --- /dev/null +++ b/002_source/cms/src/components/dashboard/mf/_repomix.md @@ -0,0 +1,9286 @@ +This file is a merged representation of the entire codebase, combined into a single document by Repomix. + +# File Summary + +## Purpose +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. + +## File Format +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Multiple file entries, each consisting of: + a. A header with the file path (## File: path/to/file) + b. The full contents of the file in a code block + +## Usage Guidelines +- 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. + +## Notes +- 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) + +## Additional Info + +# Directory Structure +``` +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 +repomix-output.xml +``` + +# Files + +## File: categories/_constants.ts +```typescript +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, +}; +``` + +## File: categories/_PROMPT.MD +```markdown +please review and add translations, e.g. `{t('[word]')}` +``` + +## File: categories/confirm-delete-modal.tsx +```typescript +'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')} + + + + + + + + +
+ ); +} +``` + +## File: categories/mf-categories-filters.tsx +```typescript +'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} + /> + + + + ); +} +``` + +## File: categories/mf-categories-pagination.tsx +```typescript +'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 ( + + ); +} +``` + +## File: categories/mf-categories-selection-context.tsx +```typescript +'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); +} +``` + +## File: categories/mf-categories-table.tsx +```typescript +'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} + + ); +} +``` + +## File: categories/mf-category-create-form.tsx +```typescript +'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)}
+
+
+ ); +} +``` + +## File: categories/mf-category-edit-form.tsx +```typescript +'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')} + + + +
+ ); +} +``` + +## File: categories/notifications.tsx +```typescript +'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} /> + + +
+
+
+ ); +} +``` + +## File: categories/payments.tsx +```typescript +'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} /> + + +
+
+
+ ); +} +``` + +## File: categories/shipping-address.tsx +```typescript +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 ? : } + + +
+
+
+ ); +} +``` + +## File: categories/type.d.ts +```typescript +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; +} +``` + +## File: questions/_constants.ts +```typescript +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, +}; +``` + +## File: questions/_PROMPT.MD +```markdown +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 +``` + +## File: questions/confirm-delete-modal.tsx +```typescript +'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')} + + + + + + + + +
+ ); +} +``` + +## File: questions/mf-question-create-form.tsx +```typescript +'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)}
+
+
+ ); +} +``` + +## File: questions/mf-question-edit-form.tsx +```typescript +'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')} + + + +
+ ); +} +``` + +## File: questions/mf-questions-filters.tsx +```typescript +'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} + /> + + + + ); +} +``` + +## File: questions/mf-questions-pagination.tsx +```typescript +'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 ( + + ); +} +``` + +## File: questions/mf-questions-selection-context.tsx +```typescript +'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); +} +``` + +## File: questions/mf-questions-table.tsx +```typescript +'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} + + ); +} +``` + +## File: questions/notifications.tsx +```typescript +'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} /> + + +
+
+
+ ); +} +``` + +## File: questions/payments.tsx +```typescript +'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} /> + + +
+
+
+ ); +} +``` + +## File: questions/shipping-address.tsx +```typescript +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 ? : } + + +
+
+
+ ); +} +``` + +## File: questions/type.d.ts +```typescript +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; +} +``` + +## File: repomix-output.xml +```xml +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; +} + + +
+``` diff --git a/002_source/cms/src/components/dashboard/student/customer-create-form.tsx b/002_source/cms/src/components/dashboard/student/customer-create-form.tsx deleted file mode 100644 index 7be8fc8..0000000 --- a/002_source/cms/src/components/dashboard/student/customer-create-form.tsx +++ /dev/null @@ -1,398 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useRouter } from 'next/navigation'; -import { zodResolver } from '@hookform/resolvers/zod'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardActions from '@mui/material/CardActions'; -import CardContent from '@mui/material/CardContent'; -import Checkbox from '@mui/material/Checkbox'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import FormHelperText from '@mui/material/FormHelperText'; -import InputLabel from '@mui/material/InputLabel'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Select from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import Grid from '@mui/material/Unstable_Grid2'; -import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; -import { Controller, useForm } from 'react-hook-form'; -import { z as zod } from 'zod'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { Option } from '@/components/core/option'; -import { toast } from '@/components/core/toaster'; - -function fileToBase64(file: Blob): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - resolve(reader.result as string); - }; - reader.onerror = () => { - reject(new Error('Error converting file to base64')); - }; - }); -} - -const schema = zod.object({ - avatar: zod.string().optional(), - name: zod.string().min(1, 'Name is required').max(255), - email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255), - phone: zod.string().min(1, 'Phone is required').max(15), - company: zod.string().max(255), - billingAddress: zod.object({ - country: zod.string().min(1, 'Country is required').max(255), - state: zod.string().min(1, 'State is required').max(255), - city: zod.string().min(1, 'City is required').max(255), - zipCode: zod.string().min(1, 'Zip code is required').max(255), - line1: zod.string().min(1, 'Street line 1 is required').max(255), - line2: zod.string().max(255).optional(), - }), - taxId: zod.string().max(255).optional(), - timezone: zod.string().min(1, 'Timezone is required').max(255), - language: zod.string().min(1, 'Language is required').max(255), - currency: zod.string().min(1, 'Currency is required').max(255), -}); - -type Values = zod.infer; - -const defaultValues = { - avatar: '', - name: '', - email: '', - phone: '', - company: '', - billingAddress: { country: '', state: '', city: '', zipCode: '', line1: '', line2: '' }, - taxId: '', - timezone: 'new_york', - language: 'en', - currency: 'USD', -} satisfies Values; - -export function CustomerCreateForm(): React.JSX.Element { - const router = useRouter(); - - const { - control, - handleSubmit, - formState: { errors }, - setValue, - watch, - } = useForm({ defaultValues, resolver: zodResolver(schema) }); - - const onSubmit = React.useCallback( - async (_: Values): Promise => { - try { - // Make API request - toast.success('Customer updated'); - router.push(paths.dashboard.customers.details('1')); - } catch (err) { - logger.error(err); - toast.error('Something went wrong!'); - } - }, - [router] - ); - - const avatarInputRef = React.useRef(null); - const avatar = watch('avatar'); - - const handleAvatarChange = React.useCallback( - async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - - if (file) { - const url = await fileToBase64(file); - setValue('avatar', url); - } - }, - [setValue] - ); - - return ( -
- - - } spacing={4}> - - Account information - - - - - - - - - - Avatar - Min 400x400px, PNG or JPEG - - - - - - - ( - - Name - - {errors.name ? {errors.name.message} : null} - - )} - /> - - - ( - - Email address - - {errors.email ? {errors.email.message} : null} - - )} - /> - - - ( - - Phone number - - {errors.phone ? {errors.phone.message} : null} - - )} - /> - - - ( - - Company - - {errors.company ? {errors.company.message} : null} - - )} - /> - - - - - Billing information - - - ( - - Country - - {errors.billingAddress?.country ? ( - {errors.billingAddress?.country?.message} - ) : null} - - )} - /> - - - ( - - State - - {errors.billingAddress?.state ? ( - {errors.billingAddress?.state?.message} - ) : null} - - )} - /> - - - ( - - City - - {errors.billingAddress?.city ? ( - {errors.billingAddress?.city?.message} - ) : null} - - )} - /> - - - ( - - Zip code - - {errors.billingAddress?.zipCode ? ( - {errors.billingAddress?.zipCode?.message} - ) : null} - - )} - /> - - - ( - - Address - - {errors.billingAddress?.line1 ? ( - {errors.billingAddress?.line1?.message} - ) : null} - - )} - /> - - - ( - - Tax ID - - {errors.taxId ? {errors.taxId.message} : null} - - )} - /> - - - - - Shipping information - } label="Same as billing address" /> - - - Additional information - - - ( - - Timezone - - {errors.timezone ? {errors.timezone.message} : null} - - )} - /> - - - ( - - Language - - {errors.language ? {errors.language.message} : null} - - )} - /> - - - ( - - Currency - - {errors.currency ? {errors.currency.message} : null} - - )} - /> - - - - - - - - - - -
- ); -} diff --git a/002_source/cms/src/components/dashboard/student/customers-filters.tsx b/002_source/cms/src/components/dashboard/student/customers-filters.tsx deleted file mode 100644 index 1567e3b..0000000 --- a/002_source/cms/src/components/dashboard/student/customers-filters.tsx +++ /dev/null @@ -1,241 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import Button from '@mui/material/Button'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Select from '@mui/material/Select'; -import type { SelectChangeEvent } from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Tab from '@mui/material/Tab'; -import Tabs from '@mui/material/Tabs'; -import Typography from '@mui/material/Typography'; - -import { paths } from '@/paths'; -import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; -import { Option } from '@/components/core/option'; - -import { useCustomersSelection } from './customers-selection-context'; - -// The tabs should be generated using API data. -const tabs = [ - { label: 'All', value: '', count: 5 }, - { label: 'Active', value: 'active', count: 3 }, - { label: 'Pending', value: 'pending', count: 1 }, - { label: 'Blocked', value: 'blocked', count: 1 }, -] as const; - -export interface Filters { - email?: string; - phone?: string; - status?: string; -} - -export type SortDir = 'asc' | 'desc'; - -export interface CustomersFiltersProps { - filters?: Filters; - sortDir?: SortDir; -} - -export function CustomersFilters({ filters = {}, sortDir = 'desc' }: CustomersFiltersProps): React.JSX.Element { - const { email, phone, status } = filters; - - const router = useRouter(); - - const selection = useCustomersSelection(); - - const updateSearchParams = React.useCallback( - (newFilters: Filters, newSortDir: SortDir): void => { - const searchParams = new URLSearchParams(); - - if (newSortDir === 'asc') { - searchParams.set('sortDir', newSortDir); - } - - if (newFilters.status) { - searchParams.set('status', newFilters.status); - } - - if (newFilters.email) { - searchParams.set('email', newFilters.email); - } - - if (newFilters.phone) { - searchParams.set('phone', newFilters.phone); - } - - router.push(`${paths.dashboard.customers.list}?${searchParams.toString()}`); - }, - [router] - ); - - const handleClearFilters = React.useCallback(() => { - updateSearchParams({}, sortDir); - }, [updateSearchParams, sortDir]); - - const handleStatusChange = React.useCallback( - (_: React.SyntheticEvent, value: string) => { - updateSearchParams({ ...filters, status: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleEmailChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, email: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handlePhoneChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, phone: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleSortChange = React.useCallback( - (event: SelectChangeEvent) => { - updateSearchParams(filters, event.target.value as SortDir); - }, - [updateSearchParams, filters] - ); - - const hasFilters = status || email || phone; - - return ( -
- - {tabs.map((tab) => ( - } - iconPosition="end" - key={tab.value} - label={tab.label} - sx={{ minHeight: 'auto' }} - tabIndex={0} - value={tab.value} - /> - ))} - - - - - { - handleEmailChange(value as string); - }} - onFilterDelete={() => { - handleEmailChange(); - }} - popover={} - value={email} - /> - { - handlePhoneChange(value as string); - }} - onFilterDelete={() => { - handlePhoneChange(); - }} - popover={} - value={phone} - /> - {hasFilters ? : null} - - {selection.selectedAny ? ( - - - {selection.selected.size} selected - - - - ) : null} - - -
- ); -} - -function EmailFilterPopover(): React.JSX.Element { - 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 PhoneFilterPopover(): React.JSX.Element { - 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} - /> - - - - ); -} diff --git a/002_source/cms/src/components/dashboard/student/customers-pagination.tsx b/002_source/cms/src/components/dashboard/student/customers-pagination.tsx deleted file mode 100644 index ab01272..0000000 --- a/002_source/cms/src/components/dashboard/student/customers-pagination.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import * as React from 'react'; -import TablePagination from '@mui/material/TablePagination'; - -function noop(): void { - return undefined; -} - -interface CustomersPaginationProps { - count: number; - page: number; -} - -export function CustomersPagination({ count, page }: CustomersPaginationProps): React.JSX.Element { - // You should implement the pagination using a similar logic as the filters. - // Note that when page change, you should keep the filter search params. - - return ( - - ); -} diff --git a/002_source/cms/src/components/dashboard/student/customers-selection-context.tsx b/002_source/cms/src/components/dashboard/student/customers-selection-context.tsx deleted file mode 100644 index 023dbc0..0000000 --- a/002_source/cms/src/components/dashboard/student/customers-selection-context.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import * as React from 'react'; - -import { useSelection } from '@/hooks/use-selection'; -import type { Selection } from '@/hooks/use-selection'; - -import type { Customer } from './customers-table'; - -function noop(): void { - return undefined; -} - -export interface CustomersSelectionContextValue extends Selection {} - -export const CustomersSelectionContext = React.createContext({ - deselectAll: noop, - deselectOne: noop, - selectAll: noop, - selectOne: noop, - selected: new Set(), - selectedAny: false, - selectedAll: false, -}); - -interface CustomersSelectionProviderProps { - children: React.ReactNode; - customers: Customer[]; -} - -export function CustomersSelectionProvider({ - children, - customers = [], -}: CustomersSelectionProviderProps): React.JSX.Element { - const customerIds = React.useMemo(() => customers.map((customer) => customer.id), [customers]); - const selection = useSelection(customerIds); - - return {children}; -} - -export function useCustomersSelection(): CustomersSelectionContextValue { - return React.useContext(CustomersSelectionContext); -} diff --git a/002_source/cms/src/components/dashboard/student/customers-table.tsx b/002_source/cms/src/components/dashboard/student/customers-table.tsx deleted file mode 100644 index bf9b01a..0000000 --- a/002_source/cms/src/components/dashboard/student/customers-table.tsx +++ /dev/null @@ -1,139 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Chip from '@mui/material/Chip'; -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 { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; -import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; -import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; - -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 { useCustomersSelection } from './customers-selection-context'; - -export interface Customer { - id: string; - name: string; - avatar?: string; - email: string; - phone?: string; - quota: number; - status: 'pending' | 'active' | 'blocked'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {' '} -
- - {row.name} - - - {row.email} - -
-
- ), - name: 'Name', - width: '250px', - }, - { - formatter: (row): React.JSX.Element => ( - - - - {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} - - - ), - name: 'Quota', - width: '250px', - }, - { field: 'phone', name: 'Phone number', width: '150px' }, - { - formatter(row) { - return dayjs(row.createdAt).format('MMM D, YYYY h:mm A'); - }, - name: 'Created at', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - active: { label: 'Active', icon: }, - blocked: { label: 'Blocked', icon: }, - pending: { label: 'Pending', icon: }, - } as const; - const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; - - return ; - }, - name: 'Status', - width: '150px', - }, - { - formatter: (): React.JSX.Element => ( - - - - ), - name: 'Actions', - hideName: true, - width: '100px', - align: 'right', - }, -] satisfies ColumnDef[]; - -export interface CustomersTableProps { - rows: Customer[]; -} - -export function CustomersTable({ rows }: CustomersTableProps): React.JSX.Element { - const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCustomersSelection(); - - return ( - - - columns={columns} - onDeselectAll={deselectAll} - onDeselectOne={(_, row) => { - deselectOne(row.id); - }} - onSelectAll={selectAll} - onSelectOne={(_, row) => { - selectOne(row.id); - }} - rows={rows} - selectable - selected={selected} - /> - {!rows.length ? ( - - - No customers found - - - ) : null} - - ); -} diff --git a/002_source/cms/src/components/dashboard/student/helloworld.tsx b/002_source/cms/src/components/dashboard/student/helloworld.tsx deleted file mode 100644 index 3989cb1..0000000 --- a/002_source/cms/src/components/dashboard/student/helloworld.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const helloworld = 'helloworld'; - -export { helloworld }; diff --git a/002_source/cms/src/components/dashboard/student/notifications.tsx b/002_source/cms/src/components/dashboard/student/notifications.tsx deleted file mode 100644 index a6c16bd..0000000 --- a/002_source/cms/src/components/dashboard/student/notifications.tsx +++ /dev/null @@ -1,101 +0,0 @@ -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import Chip from '@mui/material/Chip'; -import Select from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; - -import { dayjs } from '@/lib/dayjs'; -import { DataTable } from '@/components/core/data-table'; -import type { ColumnDef } from '@/components/core/data-table'; -import { Option } from '@/components/core/option'; - -export interface Notification { - id: string; - type: string; - status: 'delivered' | 'pending' | 'failed'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {row.type} - - ), - name: 'Type', - width: '300px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - delivered: { label: 'Delivered', color: 'success' }, - pending: { label: 'Pending', color: 'warning' }, - failed: { label: 'Failed', color: 'error' }, - } as const; - const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; - - return ; - }, - name: 'Status', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => ( - - {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} - - ), - name: 'Date', - align: 'right', - }, -] satisfies ColumnDef[]; - -export interface NotificationsProps { - notifications: Notification[]; -} - -export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { - return ( - - - - - } - title="Notifications" - /> - - - - -
- -
-
- - - columns={columns} rows={notifications} /> - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/student/payments.tsx b/002_source/cms/src/components/dashboard/student/payments.tsx deleted file mode 100644 index 0420d32..0000000 --- a/002_source/cms/src/components/dashboard/student/payments.tsx +++ /dev/null @@ -1,138 +0,0 @@ -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; -import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple'; - -import { dayjs } from '@/lib/dayjs'; -import type { ColumnDef } from '@/components/core/data-table'; -import { DataTable } from '@/components/core/data-table'; - -export interface Payment { - currency: string; - amount: number; - invoiceId: string; - status: 'pending' | 'completed' | 'canceled' | 'refunded'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)} - - ), - name: 'Amount', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - pending: { label: 'Pending', color: 'warning' }, - completed: { label: 'Completed', color: 'success' }, - canceled: { label: 'Canceled', color: 'error' }, - refunded: { label: 'Refunded', color: 'error' }, - } as const; - const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; - - return ; - }, - name: 'Status', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - return {row.invoiceId}; - }, - name: 'Invoice ID', - width: '150px', - }, - { - formatter: (row): React.JSX.Element => ( - - {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} - - ), - name: 'Date', - align: 'right', - }, -] satisfies ColumnDef[]; - -export interface PaymentsProps { - ordersValue: number; - payments: Payment[]; - refundsValue: number; - totalOrders: number; -} - -export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { - return ( - - }> - Create Payment - - } - avatar={ - - - - } - title="Payments" - /> - - - - } - spacing={3} - sx={{ justifyContent: 'space-between', p: 2 }} - > -
- - Total orders - - {new Intl.NumberFormat('en-US').format(totalOrders)} -
-
- - Orders value - - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} - -
-
- - Refunds - - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} - -
-
-
- - - columns={columns} rows={payments} /> - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/student/shipping-address.tsx b/002_source/cms/src/components/dashboard/student/shipping-address.tsx deleted file mode 100644 index 8793e5c..0000000 --- a/002_source/cms/src/components/dashboard/student/shipping-address.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from 'react'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; - -export interface Address { - id: string; - country: string; - state: string; - city: string; - zipCode: string; - street: string; - primary?: boolean; -} - -export interface ShippingAddressProps { - address: Address; -} - -export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement { - return ( - - - - - {address.street}, -
- {address.city}, {address.state}, {address.country}, -
- {address.zipCode} -
- - {address.primary ? : } - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/teacher/customer-create-form.tsx b/002_source/cms/src/components/dashboard/teacher/customer-create-form.tsx deleted file mode 100644 index 7be8fc8..0000000 --- a/002_source/cms/src/components/dashboard/teacher/customer-create-form.tsx +++ /dev/null @@ -1,398 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useRouter } from 'next/navigation'; -import { zodResolver } from '@hookform/resolvers/zod'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardActions from '@mui/material/CardActions'; -import CardContent from '@mui/material/CardContent'; -import Checkbox from '@mui/material/Checkbox'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import FormHelperText from '@mui/material/FormHelperText'; -import InputLabel from '@mui/material/InputLabel'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Select from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import Grid from '@mui/material/Unstable_Grid2'; -import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; -import { Controller, useForm } from 'react-hook-form'; -import { z as zod } from 'zod'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { Option } from '@/components/core/option'; -import { toast } from '@/components/core/toaster'; - -function fileToBase64(file: Blob): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - resolve(reader.result as string); - }; - reader.onerror = () => { - reject(new Error('Error converting file to base64')); - }; - }); -} - -const schema = zod.object({ - avatar: zod.string().optional(), - name: zod.string().min(1, 'Name is required').max(255), - email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255), - phone: zod.string().min(1, 'Phone is required').max(15), - company: zod.string().max(255), - billingAddress: zod.object({ - country: zod.string().min(1, 'Country is required').max(255), - state: zod.string().min(1, 'State is required').max(255), - city: zod.string().min(1, 'City is required').max(255), - zipCode: zod.string().min(1, 'Zip code is required').max(255), - line1: zod.string().min(1, 'Street line 1 is required').max(255), - line2: zod.string().max(255).optional(), - }), - taxId: zod.string().max(255).optional(), - timezone: zod.string().min(1, 'Timezone is required').max(255), - language: zod.string().min(1, 'Language is required').max(255), - currency: zod.string().min(1, 'Currency is required').max(255), -}); - -type Values = zod.infer; - -const defaultValues = { - avatar: '', - name: '', - email: '', - phone: '', - company: '', - billingAddress: { country: '', state: '', city: '', zipCode: '', line1: '', line2: '' }, - taxId: '', - timezone: 'new_york', - language: 'en', - currency: 'USD', -} satisfies Values; - -export function CustomerCreateForm(): React.JSX.Element { - const router = useRouter(); - - const { - control, - handleSubmit, - formState: { errors }, - setValue, - watch, - } = useForm({ defaultValues, resolver: zodResolver(schema) }); - - const onSubmit = React.useCallback( - async (_: Values): Promise => { - try { - // Make API request - toast.success('Customer updated'); - router.push(paths.dashboard.customers.details('1')); - } catch (err) { - logger.error(err); - toast.error('Something went wrong!'); - } - }, - [router] - ); - - const avatarInputRef = React.useRef(null); - const avatar = watch('avatar'); - - const handleAvatarChange = React.useCallback( - async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - - if (file) { - const url = await fileToBase64(file); - setValue('avatar', url); - } - }, - [setValue] - ); - - return ( -
- - - } spacing={4}> - - Account information - - - - - - - - - - Avatar - Min 400x400px, PNG or JPEG - - - - - - - ( - - Name - - {errors.name ? {errors.name.message} : null} - - )} - /> - - - ( - - Email address - - {errors.email ? {errors.email.message} : null} - - )} - /> - - - ( - - Phone number - - {errors.phone ? {errors.phone.message} : null} - - )} - /> - - - ( - - Company - - {errors.company ? {errors.company.message} : null} - - )} - /> - - - - - Billing information - - - ( - - Country - - {errors.billingAddress?.country ? ( - {errors.billingAddress?.country?.message} - ) : null} - - )} - /> - - - ( - - State - - {errors.billingAddress?.state ? ( - {errors.billingAddress?.state?.message} - ) : null} - - )} - /> - - - ( - - City - - {errors.billingAddress?.city ? ( - {errors.billingAddress?.city?.message} - ) : null} - - )} - /> - - - ( - - Zip code - - {errors.billingAddress?.zipCode ? ( - {errors.billingAddress?.zipCode?.message} - ) : null} - - )} - /> - - - ( - - Address - - {errors.billingAddress?.line1 ? ( - {errors.billingAddress?.line1?.message} - ) : null} - - )} - /> - - - ( - - Tax ID - - {errors.taxId ? {errors.taxId.message} : null} - - )} - /> - - - - - Shipping information - } label="Same as billing address" /> - - - Additional information - - - ( - - Timezone - - {errors.timezone ? {errors.timezone.message} : null} - - )} - /> - - - ( - - Language - - {errors.language ? {errors.language.message} : null} - - )} - /> - - - ( - - Currency - - {errors.currency ? {errors.currency.message} : null} - - )} - /> - - - - - - - - - - -
- ); -} diff --git a/002_source/cms/src/components/dashboard/teacher/customers-filters.tsx b/002_source/cms/src/components/dashboard/teacher/customers-filters.tsx deleted file mode 100644 index 1567e3b..0000000 --- a/002_source/cms/src/components/dashboard/teacher/customers-filters.tsx +++ /dev/null @@ -1,241 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import Button from '@mui/material/Button'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Select from '@mui/material/Select'; -import type { SelectChangeEvent } from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Tab from '@mui/material/Tab'; -import Tabs from '@mui/material/Tabs'; -import Typography from '@mui/material/Typography'; - -import { paths } from '@/paths'; -import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; -import { Option } from '@/components/core/option'; - -import { useCustomersSelection } from './customers-selection-context'; - -// The tabs should be generated using API data. -const tabs = [ - { label: 'All', value: '', count: 5 }, - { label: 'Active', value: 'active', count: 3 }, - { label: 'Pending', value: 'pending', count: 1 }, - { label: 'Blocked', value: 'blocked', count: 1 }, -] as const; - -export interface Filters { - email?: string; - phone?: string; - status?: string; -} - -export type SortDir = 'asc' | 'desc'; - -export interface CustomersFiltersProps { - filters?: Filters; - sortDir?: SortDir; -} - -export function CustomersFilters({ filters = {}, sortDir = 'desc' }: CustomersFiltersProps): React.JSX.Element { - const { email, phone, status } = filters; - - const router = useRouter(); - - const selection = useCustomersSelection(); - - const updateSearchParams = React.useCallback( - (newFilters: Filters, newSortDir: SortDir): void => { - const searchParams = new URLSearchParams(); - - if (newSortDir === 'asc') { - searchParams.set('sortDir', newSortDir); - } - - if (newFilters.status) { - searchParams.set('status', newFilters.status); - } - - if (newFilters.email) { - searchParams.set('email', newFilters.email); - } - - if (newFilters.phone) { - searchParams.set('phone', newFilters.phone); - } - - router.push(`${paths.dashboard.customers.list}?${searchParams.toString()}`); - }, - [router] - ); - - const handleClearFilters = React.useCallback(() => { - updateSearchParams({}, sortDir); - }, [updateSearchParams, sortDir]); - - const handleStatusChange = React.useCallback( - (_: React.SyntheticEvent, value: string) => { - updateSearchParams({ ...filters, status: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleEmailChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, email: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handlePhoneChange = React.useCallback( - (value?: string) => { - updateSearchParams({ ...filters, phone: value }, sortDir); - }, - [updateSearchParams, filters, sortDir] - ); - - const handleSortChange = React.useCallback( - (event: SelectChangeEvent) => { - updateSearchParams(filters, event.target.value as SortDir); - }, - [updateSearchParams, filters] - ); - - const hasFilters = status || email || phone; - - return ( -
- - {tabs.map((tab) => ( - } - iconPosition="end" - key={tab.value} - label={tab.label} - sx={{ minHeight: 'auto' }} - tabIndex={0} - value={tab.value} - /> - ))} - - - - - { - handleEmailChange(value as string); - }} - onFilterDelete={() => { - handleEmailChange(); - }} - popover={} - value={email} - /> - { - handlePhoneChange(value as string); - }} - onFilterDelete={() => { - handlePhoneChange(); - }} - popover={} - value={phone} - /> - {hasFilters ? : null} - - {selection.selectedAny ? ( - - - {selection.selected.size} selected - - - - ) : null} - - -
- ); -} - -function EmailFilterPopover(): React.JSX.Element { - 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 PhoneFilterPopover(): React.JSX.Element { - 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} - /> - - - - ); -} diff --git a/002_source/cms/src/components/dashboard/teacher/customers-pagination.tsx b/002_source/cms/src/components/dashboard/teacher/customers-pagination.tsx deleted file mode 100644 index ab01272..0000000 --- a/002_source/cms/src/components/dashboard/teacher/customers-pagination.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import * as React from 'react'; -import TablePagination from '@mui/material/TablePagination'; - -function noop(): void { - return undefined; -} - -interface CustomersPaginationProps { - count: number; - page: number; -} - -export function CustomersPagination({ count, page }: CustomersPaginationProps): React.JSX.Element { - // You should implement the pagination using a similar logic as the filters. - // Note that when page change, you should keep the filter search params. - - return ( - - ); -} diff --git a/002_source/cms/src/components/dashboard/teacher/customers-selection-context.tsx b/002_source/cms/src/components/dashboard/teacher/customers-selection-context.tsx deleted file mode 100644 index 023dbc0..0000000 --- a/002_source/cms/src/components/dashboard/teacher/customers-selection-context.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import * as React from 'react'; - -import { useSelection } from '@/hooks/use-selection'; -import type { Selection } from '@/hooks/use-selection'; - -import type { Customer } from './customers-table'; - -function noop(): void { - return undefined; -} - -export interface CustomersSelectionContextValue extends Selection {} - -export const CustomersSelectionContext = React.createContext({ - deselectAll: noop, - deselectOne: noop, - selectAll: noop, - selectOne: noop, - selected: new Set(), - selectedAny: false, - selectedAll: false, -}); - -interface CustomersSelectionProviderProps { - children: React.ReactNode; - customers: Customer[]; -} - -export function CustomersSelectionProvider({ - children, - customers = [], -}: CustomersSelectionProviderProps): React.JSX.Element { - const customerIds = React.useMemo(() => customers.map((customer) => customer.id), [customers]); - const selection = useSelection(customerIds); - - return {children}; -} - -export function useCustomersSelection(): CustomersSelectionContextValue { - return React.useContext(CustomersSelectionContext); -} diff --git a/002_source/cms/src/components/dashboard/teacher/customers-table.tsx b/002_source/cms/src/components/dashboard/teacher/customers-table.tsx deleted file mode 100644 index f10c26d..0000000 --- a/002_source/cms/src/components/dashboard/teacher/customers-table.tsx +++ /dev/null @@ -1,187 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Chip from '@mui/material/Chip'; -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 { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; -import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; -import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; - -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 { useCustomersSelection } from './customers-selection-context'; - -export interface Customer { - id: string; - name: string; - avatar?: string; - email: string; - phone?: string; - quota: number; - status: 'pending' | 'active' | 'blocked'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {' '} -
- - {row.name} - - - {row.email} - -
-
- ), - name: 'Name', - width: '250px', - }, - { - formatter: (row): React.JSX.Element => ( - - - - {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} - - - ), - name: 'Quota', - width: '250px', - }, - { field: 'phone', name: 'Phone number', width: '150px' }, - { - formatter(row) { - return dayjs(row.createdAt).format('MMM D, YYYY h:mm A'); - }, - name: 'Created at', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - active: { - label: 'Active', - icon: ( - - ), - }, - blocked: { label: 'Blocked', icon: }, - pending: { - label: 'Pending', - icon: ( - - ), - }, - } as const; - const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; - - return ( - - ); - }, - name: 'Status', - width: '150px', - }, - { - formatter: (): React.JSX.Element => ( - - - - ), - name: 'Actions', - hideName: true, - width: '100px', - align: 'right', - }, -] satisfies ColumnDef[]; - -export interface CustomersTableProps { - rows: Customer[]; -} - -export function CustomersTable({ rows }: CustomersTableProps): React.JSX.Element { - const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCustomersSelection(); - - return ( - - - columns={columns} - onDeselectAll={deselectAll} - onDeselectOne={(_, row) => { - deselectOne(row.id); - }} - onSelectAll={selectAll} - onSelectOne={(_, row) => { - selectOne(row.id); - }} - rows={rows} - selectable - selected={selected} - /> - {!rows.length ? ( - - - No customers found - - - ) : null} - - ); -} diff --git a/002_source/cms/src/components/dashboard/teacher/helloworld.tsx b/002_source/cms/src/components/dashboard/teacher/helloworld.tsx deleted file mode 100644 index 3989cb1..0000000 --- a/002_source/cms/src/components/dashboard/teacher/helloworld.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const helloworld = 'helloworld'; - -export { helloworld }; diff --git a/002_source/cms/src/components/dashboard/teacher/notifications.tsx b/002_source/cms/src/components/dashboard/teacher/notifications.tsx deleted file mode 100644 index a6c16bd..0000000 --- a/002_source/cms/src/components/dashboard/teacher/notifications.tsx +++ /dev/null @@ -1,101 +0,0 @@ -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import Chip from '@mui/material/Chip'; -import Select from '@mui/material/Select'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; - -import { dayjs } from '@/lib/dayjs'; -import { DataTable } from '@/components/core/data-table'; -import type { ColumnDef } from '@/components/core/data-table'; -import { Option } from '@/components/core/option'; - -export interface Notification { - id: string; - type: string; - status: 'delivered' | 'pending' | 'failed'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {row.type} - - ), - name: 'Type', - width: '300px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - delivered: { label: 'Delivered', color: 'success' }, - pending: { label: 'Pending', color: 'warning' }, - failed: { label: 'Failed', color: 'error' }, - } as const; - const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; - - return ; - }, - name: 'Status', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => ( - - {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} - - ), - name: 'Date', - align: 'right', - }, -] satisfies ColumnDef[]; - -export interface NotificationsProps { - notifications: Notification[]; -} - -export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { - return ( - - - - - } - title="Notifications" - /> - - - - -
- -
-
- - - columns={columns} rows={notifications} /> - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/teacher/payments.tsx b/002_source/cms/src/components/dashboard/teacher/payments.tsx deleted file mode 100644 index 0420d32..0000000 --- a/002_source/cms/src/components/dashboard/teacher/payments.tsx +++ /dev/null @@ -1,138 +0,0 @@ -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import CardHeader from '@mui/material/CardHeader'; -import Chip from '@mui/material/Chip'; -import Divider from '@mui/material/Divider'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; -import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple'; - -import { dayjs } from '@/lib/dayjs'; -import type { ColumnDef } from '@/components/core/data-table'; -import { DataTable } from '@/components/core/data-table'; - -export interface Payment { - currency: string; - amount: number; - invoiceId: string; - status: 'pending' | 'completed' | 'canceled' | 'refunded'; - createdAt: Date; -} - -const columns = [ - { - formatter: (row): React.JSX.Element => ( - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)} - - ), - name: 'Amount', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - const mapping = { - pending: { label: 'Pending', color: 'warning' }, - completed: { label: 'Completed', color: 'success' }, - canceled: { label: 'Canceled', color: 'error' }, - refunded: { label: 'Refunded', color: 'error' }, - } as const; - const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; - - return ; - }, - name: 'Status', - width: '200px', - }, - { - formatter: (row): React.JSX.Element => { - return {row.invoiceId}; - }, - name: 'Invoice ID', - width: '150px', - }, - { - formatter: (row): React.JSX.Element => ( - - {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} - - ), - name: 'Date', - align: 'right', - }, -] satisfies ColumnDef[]; - -export interface PaymentsProps { - ordersValue: number; - payments: Payment[]; - refundsValue: number; - totalOrders: number; -} - -export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { - return ( - - }> - Create Payment - - } - avatar={ - - - - } - title="Payments" - /> - - - - } - spacing={3} - sx={{ justifyContent: 'space-between', p: 2 }} - > -
- - Total orders - - {new Intl.NumberFormat('en-US').format(totalOrders)} -
-
- - Orders value - - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} - -
-
- - Refunds - - - {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} - -
-
-
- - - columns={columns} rows={payments} /> - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/teacher/shipping-address.tsx b/002_source/cms/src/components/dashboard/teacher/shipping-address.tsx deleted file mode 100644 index 8793e5c..0000000 --- a/002_source/cms/src/components/dashboard/teacher/shipping-address.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from 'react'; -import Button from '@mui/material/Button'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; - -export interface Address { - id: string; - country: string; - state: string; - city: string; - zipCode: string; - street: string; - primary?: boolean; -} - -export interface ShippingAddressProps { - address: Address; -} - -export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement { - return ( - - - - - {address.street}, -
- {address.city}, {address.state}, {address.country}, -
- {address.zipCode} -
- - {address.primary ? : } - - -
-
-
- ); -} diff --git a/002_source/cms/src/constants.ts b/002_source/cms/src/constants.ts index 1041a59..7f0ea8b 100644 --- a/002_source/cms/src/constants.ts +++ b/002_source/cms/src/constants.ts @@ -3,27 +3,30 @@ // e.g. COL_APPLE = "Apple" table in dbml const COL_LESSON_TYPES = 'LessonsTypes'; const COL_LESSON_CATEGORIES = 'LessonsCategories'; -const NO_VALUE = 'NO_VALUE'; -const NO_NUM = -Infinity; -const NS_LESSON_CATEGORY = 'lesson_category'; const COL_USERS = 'users'; const COL_USER_METAS = 'UserMetas'; -// -const COL_CUSTOMERS = 'Customers'; - // RULES: // do not use LP_CATEGORIES anymore +// LP, listening practice const COL_QUIZ_LP_CATEGORIES = 'QuizLPCategories'; const COL_QUIZ_LP_QUESTIONS = 'QuizLPQuestions'; -// +// MF, matching frenzy const COL_QUIZ_MF_CATEGORIES = 'QuizMFCategories'; const COL_QUIZ_MF_QUESTIONS = 'QuizMFQuestions'; - -// New CR versions +// CR, connective revision const COL_QUIZ_CR_CATEGORIES = 'QuizCRCategories'; const COL_QUIZ_CR_QUESTIONS = 'QuizCRQuestions'; -// + +// ROLES: +const COL_CUSTOMERS = 'Customers'; +const COL_TEACHERS = 'Teachers'; +const COL_STUDENTS = 'Students'; + +// FOR page display +const NO_VALUE = 'NO_VALUE'; +const NO_NUM = -Infinity; +const NS_LESSON_CATEGORY = 'lesson_category'; export { COL_LESSON_TYPES, @@ -44,5 +47,7 @@ export { COL_QUIZ_CR_QUESTIONS, // COL_CUSTOMERS, + COL_TEACHERS, + COL_STUDENTS, // }; diff --git a/002_source/cms/src/db/Customers/Delete.tsx b/002_source/cms/src/db/Customers/Delete.tsx index 63cabb8..52760f1 100644 --- a/002_source/cms/src/db/Customers/Delete.tsx +++ b/002_source/cms/src/db/Customers/Delete.tsx @@ -1,6 +1,6 @@ import { pb } from '@/lib/pb'; import { COL_CUSTOMERS } from '@/constants'; -export async function deleteCustomer(id) { +export async function deleteCustomer(id: string): Promise { return pb.collection(COL_CUSTOMERS).delete(id); } diff --git a/002_source/cms/src/db/Customers/GetAll.tsx b/002_source/cms/src/db/Customers/GetAll.tsx index f88ef0d..1130771 100644 --- a/002_source/cms/src/db/Customers/GetAll.tsx +++ b/002_source/cms/src/db/Customers/GetAll.tsx @@ -1,6 +1,7 @@ import { pb } from '@/lib/pb'; import { COL_CUSTOMERS } from '@/constants'; +import { RecordModel } from 'pocketbase'; -export async function getAllCustomers(options = {}) { +export async function getAllCustomers(options = {}): Promise { return pb.collection(COL_CUSTOMERS).getFullList(options); } diff --git a/002_source/cms/src/db/Customers/GetAllCount.tsx b/002_source/cms/src/db/Customers/GetAllCount.tsx index 139bd7a..a10b7a3 100644 --- a/002_source/cms/src/db/Customers/GetAllCount.tsx +++ b/002_source/cms/src/db/Customers/GetAllCount.tsx @@ -1,7 +1,7 @@ import { pb } from '@/lib/pb'; import { COL_CUSTOMERS } from '@/constants'; -export async function getAllCustomersCount() { +export async function getAllCustomersCount(): Promise { const result = await pb.collection(COL_CUSTOMERS).getList(1, 1); return result.totalItems; } diff --git a/002_source/cms/src/db/Customers/GetById.tsx b/002_source/cms/src/db/Customers/GetById.tsx index d8392c1..7c8ef98 100644 --- a/002_source/cms/src/db/Customers/GetById.tsx +++ b/002_source/cms/src/db/Customers/GetById.tsx @@ -1,6 +1,7 @@ import { pb } from '@/lib/pb'; import { COL_CUSTOMERS } from '@/constants'; +import { RecordModel } from 'pocketbase'; -export async function getCustomerById(id) { +export async function getCustomerById(id: string): Promise { return pb.collection(COL_CUSTOMERS).getOne(id); } diff --git a/002_source/cms/src/db/Customers/GetHiddenCount.tsx b/002_source/cms/src/db/Customers/GetHiddenCount.tsx deleted file mode 100644 index 5352394..0000000 --- a/002_source/cms/src/db/Customers/GetHiddenCount.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { pb } from '@/lib/pb'; -import { COL_CUSTOMERS } from '@/constants'; - -export async function getHiddenCustomersCount() { - const result = await pb.collection(COL_CUSTOMERS).getList(1, 1, { - filter: 'hidden = true', - }); - return result.totalItems; -} diff --git a/002_source/cms/src/db/Customers/GetVisibleCount.tsx b/002_source/cms/src/db/Customers/GetVisibleCount.tsx deleted file mode 100644 index f610a97..0000000 --- a/002_source/cms/src/db/Customers/GetVisibleCount.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { pb } from '@/lib/pb'; -import { COL_CUSTOMERS } from '@/constants'; - -export async function getVisibleCustomersCount() { - const result = await pb.collection(COL_CUSTOMERS).getList(1, 1, { - filter: 'hidden = false', - }); - return result.totalItems; -} diff --git a/002_source/cms/src/db/Customers/Update.tsx b/002_source/cms/src/db/Customers/Update.tsx index a42caab..7beeff7 100644 --- a/002_source/cms/src/db/Customers/Update.tsx +++ b/002_source/cms/src/db/Customers/Update.tsx @@ -1,8 +1,8 @@ import { pb } from '@/lib/pb'; import { COL_CUSTOMERS } from '@/constants'; import type { RecordModel } from 'pocketbase'; -import type { CreateForm } from '@/components/dashboard/customer/type.d'; +import type { EditFormProps } from '@/components/dashboard/customer/type.d'; -export async function updateCustomer(id: string, data: Partial): Promise { +export async function updateCustomer(id: string, data: Partial): Promise { return pb.collection(COL_CUSTOMERS).update(id, data); } diff --git a/002_source/cms/src/db/Customers/_GUIDELINES.md b/002_source/cms/src/db/Customers/_GUIDELINES.md new file mode 100644 index 0000000..6515d08 --- /dev/null +++ b/002_source/cms/src/db/Customers/_GUIDELINES.md @@ -0,0 +1,31 @@ +# GUIDELINES + +This folder contains drivers for `Customer`/`Customers` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx, GetActiveCount.tsx, GetBlockedCount.tsx, GetPendingCount.tsx) +- misc (Helloworld.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; + +export async function createCustomer(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` diff --git a/002_source/cms/src/db/Events/_GUIDELINES.md b/002_source/cms/src/db/Events/_GUIDELINES.md new file mode 100644 index 0000000..90fb371 --- /dev/null +++ b/002_source/cms/src/db/Events/_GUIDELINES.md @@ -0,0 +1,33 @@ +# GUIDELINES + +This folder contains drivers for `Event`/`Events` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Events/type.d.tsx` +- Event records require special handling for: + - Date/time validation + - Location data + - Attendee management + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createEvent(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Events')) +} +``` diff --git a/002_source/cms/src/db/Helloworlds/_GUIDELINES.md b/002_source/cms/src/db/Helloworlds/_GUIDELINES.md new file mode 100644 index 0000000..4461080 --- /dev/null +++ b/002_source/cms/src/db/Helloworlds/_GUIDELINES.md @@ -0,0 +1,30 @@ +# GUIDELINES + +This folder contains test drivers for `Helloworld` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function +- type information defined in `@/db/Helloworlds/type.d.tsx` +- This is a test collection - keep implementations simple + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createHelloworld(data: CreateFormProps) { + // Simple test implementation + return pb.collection('Helloworlds').create(data); +} +``` diff --git a/002_source/cms/src/db/LessonCategories/_GUIDELINES.md b/002_source/cms/src/db/LessonCategories/_GUIDELINES.md new file mode 100644 index 0000000..96b23fd --- /dev/null +++ b/002_source/cms/src/db/LessonCategories/_GUIDELINES.md @@ -0,0 +1,30 @@ +# GUIDELINES + +This folder contains drivers for `LessonCategory`/`LessonCategories` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/LessonCategories/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_LESSON_CATEGORIES } from '@/constants'; + +export async function createLessonCategory(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` diff --git a/002_source/cms/src/db/LessonTypes/_GUIDELINES.md b/002_source/cms/src/db/LessonTypes/_GUIDELINES.md new file mode 100644 index 0000000..247a62d --- /dev/null +++ b/002_source/cms/src/db/LessonTypes/_GUIDELINES.md @@ -0,0 +1,30 @@ +# GUIDELINES + +This folder contains drivers for `LessonType`/`LessonTypes` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/LessonTypes/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_LESSON_TYPES } from '@/constants'; + +export async function createLessonType(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` diff --git a/002_source/cms/src/db/Messages/_GUIDELINES.md b/002_source/cms/src/db/Messages/_GUIDELINES.md new file mode 100644 index 0000000..27334a4 --- /dev/null +++ b/002_source/cms/src/db/Messages/_GUIDELINES.md @@ -0,0 +1,33 @@ +# GUIDELINES + +This folder contains drivers for `Message`/`Messages` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Messages/type.d.tsx` +- Message records require special handling for: + - Sender/receiver validation + - Timestamp management + - Read status tracking + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createMessage(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Messages')) +} +``` diff --git a/002_source/cms/src/db/QuizCRCategories/_GUIDELINES.md b/002_source/cms/src/db/QuizCRCategories/_GUIDELINES.md new file mode 100644 index 0000000..49057be --- /dev/null +++ b/002_source/cms/src/db/QuizCRCategories/_GUIDELINES.md @@ -0,0 +1,31 @@ +# GUIDELINES + +This folder contains drivers for `QuizCRCategory`/`QuizCRCategories` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/QuizCRCategories/type.d.tsx` +- Quiz categories may require additional validation logic + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; + +export async function createQuizCRCategory(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` diff --git a/002_source/cms/src/db/QuizLPCategories/_GUIDELINES.md b/002_source/cms/src/db/QuizLPCategories/_GUIDELINES.md new file mode 100644 index 0000000..5de26a5 --- /dev/null +++ b/002_source/cms/src/db/QuizLPCategories/_GUIDELINES.md @@ -0,0 +1,31 @@ +# GUIDELINES + +This folder contains drivers for `QuizLPCategory` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/QuizLPCategories/type.d.tsx` +- Quiz LP categories may require additional validation logic + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +export async function createQuizLPCategory(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(COL_QUIZ_LP_CATEGORIES)) +} +``` diff --git a/002_source/cms/src/db/QuizLPCategories/_PROMPT.md b/002_source/cms/src/db/QuizLPCategories/_PROMPT.md deleted file mode 100644 index 2041c00..0000000 --- a/002_source/cms/src/db/QuizLPCategories/_PROMPT.md +++ /dev/null @@ -1,18 +0,0 @@ -please help to review the `tsx` file in this folder -`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPCategories` - -it was clone from -`MFCategories` -please help to modify to -`LPCategories` - -please also help to modify the name of -`variables`, `constants`, `functions`, `classes`, components's name, paths - -the db fields structures between them are the same - -do not move the files -do not create directories -keep current folder structure is important - -thanks diff --git a/002_source/cms/src/db/QuizLPQuestions/_GUIDELINES.md b/002_source/cms/src/db/QuizLPQuestions/_GUIDELINES.md new file mode 100644 index 0000000..b1cc44e --- /dev/null +++ b/002_source/cms/src/db/QuizLPQuestions/_GUIDELINES.md @@ -0,0 +1,34 @@ +# GUIDELINES + +This folder contains drivers for `QuizLPQuestion` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/QuizLPQuestions/type.d.tsx` +- Quiz LP questions require special handling for: + - Answer validation + - Question type checking + - Category association + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +export async function createQuizLPQuestion(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(COL_QUIZ_LP_QUESTIONS)) +} +``` diff --git a/002_source/cms/src/db/Subscriptions/_GUIDELINES.md b/002_source/cms/src/db/Subscriptions/_GUIDELINES.md new file mode 100644 index 0000000..b28f07d --- /dev/null +++ b/002_source/cms/src/db/Subscriptions/_GUIDELINES.md @@ -0,0 +1,33 @@ +# GUIDELINES + +This folder contains drivers for `Subscription`/`Subscriptions` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Subscriptions/type.d.tsx` +- Subscription records require special handling for: + - Payment status validation + - Expiration date checks + - Auto-renewal logic + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createSubscription(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Subscriptions')) +} +``` diff --git a/002_source/cms/src/db/UserMetas/_GUIDELINES.md b/002_source/cms/src/db/UserMetas/_GUIDELINES.md new file mode 100644 index 0000000..40c2a38 --- /dev/null +++ b/002_source/cms/src/db/UserMetas/_GUIDELINES.md @@ -0,0 +1,30 @@ +# GUIDELINES + +This folder contains drivers for `UserMeta`/`UserMetas` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/UserMetas/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_USER_METAS } from '@/constants'; + +export async function createUserMeta(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` diff --git a/002_source/cms/src/db/Customers/_NOTES.md b/002_source/cms/src/db/Users/_GUIDELINES.md similarity index 67% rename from 002_source/cms/src/db/Customers/_NOTES.md rename to 002_source/cms/src/db/Users/_GUIDELINES.md index 6fb9118..17cc639 100644 --- a/002_source/cms/src/db/Customers/_NOTES.md +++ b/002_source/cms/src/db/Users/_GUIDELINES.md @@ -1,11 +1,11 @@ -# NOTES +# GUIDELINES -this folder containing driver for `Customer` / `Customers` record: +This folder contains drivers for `User`/`Users` records using PocketBase: - create (Create.tsx) - read (GetById.tsx) - write (Update.tsx) -- count (GetAllCount.tsx, GetHiddenCount.tsx, GetVisibleCount.tsx) +- count (GetAllCount.tsx) - delete (Delete.tsx) - list (GetAll.tsx) @@ -15,14 +15,15 @@ the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-onlin - assume `pb` is located in `@/lib/pb` - no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Users/type.d.tsx` simple template: ```typescript import { pb } from '@/lib/pb'; -import { COL_CUSTOMERS } from '@/constants'; +import { COL_USERS } from '@/constants'; -export async function createCustomer(data) { +export async function createUser(data: CreateFormProps) { // ...content // use direct return of pb.collection (e.g. return pb.collection(xxx)) } diff --git a/002_source/cms/src/db/_repomix.md b/002_source/cms/src/db/_repomix.md new file mode 100644 index 0000000..297f887 --- /dev/null +++ b/002_source/cms/src/db/_repomix.md @@ -0,0 +1,12508 @@ +This file is a merged representation of the entire codebase, combined into a single document by Repomix. + +# File Summary + +## Purpose +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. + +## File Format +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Multiple file entries, each consisting of: + a. A header with the file path (## File: path/to/file) + b. The full contents of the file in a code block + +## Usage Guidelines +- 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. + +## Notes +- 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) + +## Additional Info + +# Directory Structure +``` +_PROMPT/ + 1.MD + 2.MD + 3.MD + 4.md + temp.md +Customers/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetActiveCount.tsx + GetAll.tsx + GetAllCount.tsx + GetBlockedCount.tsx + GetById.tsx + GetPendingCount.tsx + Helloworld.tsx + Update.tsx +Events/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Update.tsx.draft +Helloworlds/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Helloworld.tsx.draft + Update.tsx.draft +LessonCategories/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Helloworld.tsx + Update.tsx +LessonTypes/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Helloworld.tsx + Update.tsx +Messages/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Update.tsx.draft +QuizCategories/ + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Helloworld.tsx.draft + Update.tsx.draft +QuizConnectives/ + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Helloworld.tsx.draft + Update.tsx.draft +QuizConnectivesCategories/ + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Helloworld.tsx.draft + Update.tsx.draft +QuizCRCategories/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Update.tsx +QuizCRQuestions/ + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx +QuizListenings/ + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + ListWithOption.tsx +QuizLPCategories/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Update.tsx +QuizLPQuestions/ + _GUIDELINES.md + _PROMPT.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Update.tsx +QuizMFCategories/ + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Update.tsx +Students/ + Create.tsx + Delete.tsx + GetActiveCount.tsx + GetAll.tsx + GetAllCount.tsx + GetBlockedCount.tsx + GetById.tsx + Update.tsx +Subscriptions/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Update.tsx.draft +Teachers/ + _GUIDELINES.md + Create.tsx + GetActiveCount.tsx + GetAll.tsx + GetAllCount.tsx + GetBlockedCount.tsx + GetById.tsx + GetPendingCount.tsx + Update.tsx +UserMetas/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx + GetById.tsx.draft + Update.tsx.draft +Users/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx + GetById.tsx.draft + Update.tsx.draft +DB_AI_GUIDELINE.MD +repomix-output.xml +schema.json +``` + +# Files + +## File: _PROMPT/1.MD +````markdown +Hi, please study the documentation below, +i will send you the task afterwards, + +please read and understand the documentation below and link up the ideas +reply `OK` when you done +no need to state me any other things, thanks + +1. `schema.dbml` + +- this describe the database schema in dbml format +- filepath: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml` + +2. `schema.json` + +- this is the schema export in pocketbase format +- filepath: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/schema.json` + +3. `_AI_GUIDELINE`: + +- there are the markdown files that help you better understand the implementation +- directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/_AI_GUIDELINE` + +thanks + +--- + +# task + +clone from `LessonTypes` to `Customers` + +## steps + +1. read `tsx` files from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/LessonTypes` +1. copy file to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers` +1. modify the copied `tsx` files to suit `customer` fields +```` + +## File: _PROMPT/2.MD +````markdown +update `LpCategoryDefaultValue` +in file `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.ts` + +thanks + +you can find the type def in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` + +please help to draft code file: + +base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db` + +using +`$base_dir/QuizListenings/GetHiddenCount.tsx`, +`$base_dir/QuizListenings/GetVisibleCount.tsx`, +`$base_dir/LessonTypes/GetHiddenCount.tsx`, +`$base_dir/LessonTypes/GetVisibleCount.tsx`, +as reference, + +look into the all directories under base_dir e.g. `QuizCategories`. +propergate `GetHiddenCount.tsx` and `GetVisibleCount.tsx` if missing, do the change to suit the collection. +use `.draft.tsx` instead when you write file + +--- + +rewrite `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/LessonCategories/GetAllCount.tsx` to match `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/UserMetas/GetAllCount.tsx` style + +--- + +style rewrite + +study +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/overview/summary/ActiveUserCount/index.tsx` +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/overview/summary/LessonCategoriesCount/index.tsx` + +and rewrite `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/overview/summary/LessonTypeCount/index.tsx` to match style above thanks +```` + +## File: _PROMPT/3.MD +````markdown +please draft with idea: + +``` +await pb +.collection(COL_LESSON_TYPES) +.getList(currentPage + 1, rowsPerPage, listOption); +``` + +for Listening Practice + +thanks + +I want you to clone +from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/LessonTypes/GetVisibleCount.tsx` (source file) + +to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizListenings/GetVisibleCount.tsx` (dest file) + +please extract , link up and remember the document properties +(e.g. types, functions, variables, constants, etc) +from source file +draft dest file + +update the variables and properties of dest file to reflect `listening practice categories`/`lp_categories` + +--- + +## task + +update `schema.dbml` to reflect `schema.json` + +## details + +Hi, +I have a pocketbase export json file: +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/pocketbase/pb_hooks/seed/schema.json` + +and a dbml file: +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml` + +the collection name in pocketbase should be reflected by a table in dbml, + +## steps + +compare `schema.json` and `schema.dbml` +please keep `schema.json` remain unchanged +update `schema.dbml` to reflect `schema.json` +do check again when finished +```` + +## File: _PROMPT/4.md +````markdown +--- + +clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it + +please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden` + +well done !, please proceed to another request + +working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db` + +according information from `schema.json`, get the collection of `Students` + +pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content + +when you draft coding, review file and append with `.tsx.draft` + +--- + +- this is part of react typescript project, with pocketbase +- `schema.dbml`, describe the collections(tables) +- folder `LessonCategories`, the correct references +- folder `LessonTypes`, the correct references +- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006` +- do not read root directory, assume it is a fresh copy of nextjs project is ok + +## instruction + +- break the questions into smaller parts +- review file append with `.draft`, see if the content aligned with the correct references +- read and understand `dbml` file +- lookup the every folder + +## tasks + +Thanks + +--- + +please take a look in `schema.dbml` and `schema.json`, +associate the collection from json file to the table in dbml file + +please modify the `schema.dbml` to align with `schema.json` + +to the collection `QuizLPCategories` align the dbml file in the previous prompt + +--- + +please revise + +please revise +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory` + +to the collection `QuizLPCategories` align the dbml file in the previous prompt + +please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx` + +to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory` +```` + +## File: _PROMPT/temp.md +````markdown +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/lp-categories-filters.tsx` + +this file is original for `lesson_category` model, +please modify it to fit `lp_category` (listening practice category) + +thanks +```` + +## File: Customers/_GUIDELINES.md +````markdown +# GUIDELINES + +This folder contains drivers for `Customer`/`Customers` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx, GetActiveCount.tsx, GetBlockedCount.tsx, GetPendingCount.tsx) +- misc (Helloworld.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; + +export async function createCustomer(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` +```` + +## File: Customers/Create.tsx +````typescript +// api method for crate customer record +// RULES: +// TBA +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; +import type { CreateFormProps } from '@/components/dashboard/customer/type.d'; +import type { RecordModel } from 'pocketbase'; + +export async function createCustomer(data: CreateFormProps): Promise { + return pb.collection(COL_CUSTOMERS).create(data); +} +```` + +## File: Customers/Delete.tsx +````typescript +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; + +export async function deleteCustomer(id: string): Promise { + return pb.collection(COL_CUSTOMERS).delete(id); +} +```` + +## File: Customers/GetActiveCount.tsx +````typescript +import { COL_CUSTOMERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetActiveCount(): Promise { + const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, { + filter: 'status = "active"', + }); + return count; +} +```` + +## File: Customers/GetAll.tsx +````typescript +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getAllCustomers(options = {}): Promise { + return pb.collection(COL_CUSTOMERS).getFullList(options); +} +```` + +## File: Customers/GetAllCount.tsx +````typescript +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; + +export async function getAllCustomersCount(): Promise { + const result = await pb.collection(COL_CUSTOMERS).getList(1, 1); + return result.totalItems; +} +```` + +## File: Customers/GetBlockedCount.tsx +````typescript +import { COL_CUSTOMERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetBlockedCount(): Promise { + const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, { + filter: 'status = "blocked"', + }); + return count; +} +```` + +## File: Customers/GetById.tsx +````typescript +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getCustomerById(id: string): Promise { + return pb.collection(COL_CUSTOMERS).getOne(id); +} +```` + +## File: Customers/GetPendingCount.tsx +````typescript +import { COL_CUSTOMERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetPendingCount(): Promise { + const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, { + filter: 'status = "pending"', + }); + return count; +} +```` + +## File: Customers/Helloworld.tsx +````typescript +export function helloCustomer() { + return 'Hello from Customers module!'; +} +```` + +## File: Customers/Update.tsx +````typescript +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import type { EditFormProps } from '@/components/dashboard/customer/type.d'; + +export async function updateCustomer(id: string, data: Partial): Promise { + return pb.collection(COL_CUSTOMERS).update(id, data); +} +```` + +## File: Events/_GUIDELINES.md +````markdown +# GUIDELINES + +This folder contains drivers for `Event`/`Events` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Events/type.d.tsx` +- Event records require special handling for: + - Date/time validation + - Location data + - Attendee management + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createEvent(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Events')) +} +``` +```` + +## File: Events/Create.tsx.draft +```` +import { COL_EVENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/event/types'; + +export default function createEvent(data: CreateForm): Promise { + return pb.collection(COL_EVENTS).create(data); +} +```` + +## File: Events/Delete.tsx.draft +```` +import { COL_EVENTS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteEvent(id: string): Promise { + return pb.collection(COL_EVENTS).delete(id); +} +```` + +## File: Events/GetAll.tsx.draft +```` +import { COL_EVENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllEvents(): Promise { + return pb.collection(COL_EVENTS).getFullList({ + sort: 'event_time' // Sort by event time + }); +} +```` + +## File: Events/GetAllCount.tsx.draft +```` +import { COL_EVENTS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getAllEventsCount(startDate?: string, endDate?: string): Promise { + let filter = ''; + if (startDate && endDate) { + filter = `event_time >= "${startDate}" && event_time <= "${endDate}"`; + } else if (startDate) { + filter = `event_time >= "${startDate}"`; + } else if (endDate) { + filter = `event_time <= "${endDate}"`; + } + + const { totalItems: count } = await pb + .collection(COL_EVENTS) + .getList(1, 9999, { filter }); + return count; +} +```` + +## File: Events/GetById.tsx.draft +```` +import { COL_EVENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getEventById(id: string): Promise { + return pb.collection(COL_EVENTS).getOne(id); +} +```` + +## File: Events/GetHiddenCount.tsx.draft +```` +// REQ0006 +import { COL_EVENTS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_EVENTS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: Events/GetVisibleCount.tsx.draft +```` +// REQ0006 +import { COL_EVENTS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_EVENTS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: Events/Update.tsx.draft +```` +import { COL_EVENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { UpdateForm } from '@/components/dashboard/event/types'; + +export default function updateEvent(id: string, data: UpdateForm): Promise { + return pb.collection(COL_EVENTS).update(id, data); +} +```` + +## File: Helloworlds/_GUIDELINES.md +````markdown +# GUIDELINES + +This folder contains test drivers for `Helloworld` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function +- type information defined in `@/db/Helloworlds/type.d.tsx` +- This is a test collection - keep implementations simple + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createHelloworld(data: CreateFormProps) { + // Simple test implementation + return pb.collection('Helloworlds').create(data); +} +``` +```` + +## File: Helloworlds/Create.tsx.draft +```` +import { COL_HELLOWORLDS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/helloworld/types'; + +export default function createHelloworld(data: CreateForm): Promise { + return pb.collection(COL_HELLOWORLDS).create(data); +} +```` + +## File: Helloworlds/Delete.tsx.draft +```` +import { COL_HELLOWORLDS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default function deleteHelloworld(id: string): Promise { + return pb.collection(COL_HELLOWORLDS).delete(id); +} +```` + +## File: Helloworlds/GetAll.tsx.draft +```` +import { COL_HELLOWORLDS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllHelloworlds(): Promise { + return pb.collection(COL_HELLOWORLDS).getFullList(); +} +```` + +## File: Helloworlds/GetById.tsx.draft +```` +import { COL_HELLOWORLDS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getHelloworldById(id: string): Promise { + return pb.collection(COL_HELLOWORLDS).getOne(id); +} +```` + +## File: Helloworlds/GetHiddenCount.tsx.draft +```` +// REQ0006 +import { COL_HELLOWORLDS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_HELLOWORLDS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: Helloworlds/GetVisibleCount.tsx.draft +```` +// REQ0006 +import { COL_HELLOWORLDS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_HELLOWORLDS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: Helloworlds/Helloworld.tsx.draft +```` +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; +```` + +## File: Helloworlds/Update.tsx.draft +```` +import { COL_HELLOWORLDS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/helloworld/types'; + +export default function updateHelloworld(id: string, data: CreateForm): Promise { + return pb.collection(COL_HELLOWORLDS).update(id, data); +} +```` + +## File: LessonCategories/_GUIDELINES.md +````markdown +# GUIDELINES + +This folder contains drivers for `LessonCategory`/`LessonCategories` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/LessonCategories/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_LESSON_CATEGORIES } from '@/constants'; + +export async function createLessonCategory(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` +```` + +## File: LessonCategories/Create.tsx +````typescript +import { COL_LESSON_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function createLessonCategory(data: CreateFormProps): Promise { + return pb.collection(COL_LESSON_CATEGORIES).create(data); +} +```` + +## File: LessonCategories/Delete.tsx +````typescript +import { COL_LESSON_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteLessonCategory(id: string): Promise { + return pb.collection(COL_LESSON_CATEGORIES).delete(id); +} +```` + +## File: LessonCategories/GetAll.tsx +````typescript +import { COL_LESSON_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllLessonCategories(): Promise { + return pb.collection(COL_LESSON_CATEGORIES).getFullList(); +} +```` + +## File: LessonCategories/GetAllCount.tsx +````typescript +// RULES: +// error handled by caller +// contain definition to collection only + +import { COL_LESSON_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function getAllLessonCategoriesCount(): Promise { + return pb + .collection(COL_LESSON_CATEGORIES) + .getList(1, 9999) + .then((res) => res.totalItems); +} +```` + +## File: LessonCategories/GetById.tsx +````typescript +import { COL_LESSON_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getLessonCategoryById(id: string): Promise { + return pb.collection(COL_LESSON_CATEGORIES).getOne(id); +} +```` + +## File: LessonCategories/GetHiddenCount.tsx +````typescript +// REQ0006 +import { COL_LESSON_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: LessonCategories/GetVisibleCount.tsx +````typescript +// REQ0006 +import { COL_LESSON_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: LessonCategories/Helloworld.tsx +````typescript +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; +```` + +## File: LessonCategories/Update.tsx +````typescript +import { COL_LESSON_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function updateLessonCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_LESSON_CATEGORIES).update(id, data); +} +```` + +## File: LessonTypes/_GUIDELINES.md +````markdown +# GUIDELINES + +This folder contains drivers for `LessonType`/`LessonTypes` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/LessonTypes/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_LESSON_TYPES } from '@/constants'; + +export async function createLessonType(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` +```` + +## File: LessonTypes/Create.tsx +````typescript +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/lesson_type/lesson-type'; + +export default function createLessonType(data: CreateForm): Promise { + return pb.collection(COL_LESSON_TYPES).create(data); +} +```` + +## File: LessonTypes/Delete.tsx +````typescript +import { COL_LESSON_TYPES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteLessonType(id: string): Promise { + return pb.collection(COL_LESSON_TYPES).delete(id); +} +```` + +## File: LessonTypes/GetAll.tsx +````typescript +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllLessonTypes(): Promise { + return pb.collection(COL_LESSON_TYPES).getFullList(); +} +```` + +## File: LessonTypes/GetAllCount.tsx +````typescript +// REQ0006 +import { COL_LESSON_TYPES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_TYPES).getList(1, 9999, {}); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: LessonTypes/GetById.tsx +````typescript +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getLessonTypeById(id: string): Promise { + return pb.collection(COL_LESSON_TYPES).getOne(id); +} +```` + +## File: LessonTypes/GetHiddenCount.tsx +````typescript +// REQ0006 +import { COL_LESSON_TYPES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_TYPES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: LessonTypes/GetVisibleCount.tsx +````typescript +// REQ0006 +import { COL_LESSON_TYPES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_TYPES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: LessonTypes/Helloworld.tsx +````typescript +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; +```` + +## File: LessonTypes/Update.tsx +````typescript +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/lesson_type/lesson-type'; + +export default function updateLessonType(id: string, data: CreateForm): Promise { + return pb.collection(COL_LESSON_TYPES).update(id, data); +} +```` + +## File: Messages/_GUIDELINES.md +````markdown +# GUIDELINES + +This folder contains drivers for `Message`/`Messages` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Messages/type.d.tsx` +- Message records require special handling for: + - Sender/receiver validation + - Timestamp management + - Read status tracking + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createMessage(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Messages')) +} +``` +```` + +## File: Messages/Create.tsx.draft +```` +import { COL_MESSAGES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/message/types'; + +export default function createMessage(data: CreateForm): Promise { + return pb.collection(COL_MESSAGES).create(data); +} +```` + +## File: Messages/Delete.tsx.draft +```` +import { COL_MESSAGES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteMessage(id: string): Promise { + return pb.collection(COL_MESSAGES).delete(id); +} +```` + +## File: Messages/GetAll.tsx.draft +```` +import { COL_MESSAGES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllMessages(): Promise { + return pb.collection(COL_MESSAGES).getFullList({ + expand: 'user_id', // Expand related user data + sort: '-created' // Sort by most recent first + }); +} +```` + +## File: Messages/GetAllCount.tsx.draft +```` +import { COL_MESSAGES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getAllMessagesCount(status?: string): Promise { + const filter = status ? `status = "${status}"` : ''; + const { totalItems: count } = await pb + .collection(COL_MESSAGES) + .getList(1, 9999, { filter }); + return count; +} +```` + +## File: Messages/GetById.tsx.draft +```` +import { COL_MESSAGES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getMessageById(id: string): Promise { + return pb.collection(COL_MESSAGES).getOne(id, { + expand: 'user_id' // Expand related user data + }); +} +```` + +## File: Messages/GetHiddenCount.tsx.draft +```` +// REQ0006 +import { COL_MESSAGES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_MESSAGES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: Messages/GetVisibleCount.tsx.draft +```` +// REQ0006 +import { COL_MESSAGES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_MESSAGES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: Messages/Update.tsx.draft +```` +import { COL_MESSAGES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { UpdateForm } from '@/components/dashboard/message/types'; + +export default function updateMessage(id: string, data: UpdateForm): Promise { + return pb.collection(COL_MESSAGES).update(id, data); +} +```` + +## File: QuizCategories/Create.tsx.draft +```` +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface CreateForm { + // TODO: Add QuizCategories fields +} + +export default function createQuizCategory(data: CreateForm): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).create(data); +} +```` + +## File: QuizCategories/Delete.tsx.draft +```` +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizCategory(id: string): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).delete(id); +} +```` + +## File: QuizCategories/GetAll.tsx.draft +```` +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizCategories(): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).getFullList(); +} +```` + +## File: QuizCategories/GetById.tsx.draft +```` +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).getOne(id); +} +```` + +## File: QuizCategories/GetHiddenCount.tsx.draft +```` +// REQ0006 +import { COL_QUIZ_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizCategories/GetVisibleCount.tsx.draft +```` +// REQ0006 +import { COL_QUIZ_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizCategories/Helloworld.tsx.draft +```` +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; +```` + +## File: QuizCategories/Update.tsx.draft +```` +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface UpdateForm { + // TODO: Add QuizCategories fields +} + +export default function updateQuizCategory(id: string, data: UpdateForm): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).update(id, data); +} +```` + +## File: QuizConnectives/Create.tsx.draft +```` +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface CreateForm { + // TODO: Add QuizConnectives fields +} + +export default function createQuizConnective(data: CreateForm): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).create(data); +} +```` + +## File: QuizConnectives/Delete.tsx.draft +```` +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizConnective(id: string): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).delete(id); +} +```` + +## File: QuizConnectives/GetAll.tsx.draft +```` +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizConnectives(): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).getFullList(); +} +```` + +## File: QuizConnectives/GetById.tsx.draft +```` +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizConnectiveById(id: string): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).getOne(id); +} +```` + +## File: QuizConnectives/GetHiddenCount.tsx.draft +```` +// REQ0006 +import { COL_QUIZ_CONNECTIVES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CONNECTIVES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizConnectives/GetVisibleCount.tsx.draft +```` +// REQ0006 +import { COL_QUIZ_CONNECTIVES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CONNECTIVES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizConnectives/Helloworld.tsx.draft +```` +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; +```` + +## File: QuizConnectives/Update.tsx.draft +```` +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface UpdateForm { + // TODO: Add QuizConnectives fields +} + +export default function updateQuizConnective(id: string, data: UpdateForm): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).update(id, data); +} +```` + +## File: QuizConnectivesCategories/Create.tsx.draft +```` +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface CreateForm { + // TODO: Add QuizConnectivesCategories fields +} + +export default function createQuizConnectivesCategory(data: CreateForm): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).create(data); +} +```` + +## File: QuizConnectivesCategories/Delete.tsx.draft +```` +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizConnectivesCategory(id: string): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).delete(id); +} +```` + +## File: QuizConnectivesCategories/GetAll.tsx.draft +```` +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizConnectivesCategories(): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).getFullList(); +} +```` + +## File: QuizConnectivesCategories/GetById.tsx.draft +```` +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizConnectivesCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).getOne(id); +} +```` + +## File: QuizConnectivesCategories/GetHiddenCount.tsx.draft +```` +// REQ0006 +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb + .collection(COL_QUIZ_CONNECTIVES_CATEGORIES) + .getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizConnectivesCategories/GetVisibleCount.tsx.draft +```` +// REQ0006 +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb + .collection(COL_QUIZ_CONNECTIVES_CATEGORIES) + .getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizConnectivesCategories/Helloworld.tsx.draft +```` +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; +```` + +## File: QuizConnectivesCategories/Update.tsx.draft +```` +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface UpdateForm { + // TODO: Add QuizConnectivesCategories fields +} + +export default function updateQuizConnectivesCategory(id: string, data: UpdateForm): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).update(id, data); +} +```` + +## File: QuizCRCategories/_GUIDELINES.md +````markdown +# GUIDELINES + +This folder contains drivers for `QuizCRCategory`/`QuizCRCategories` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/QuizCRCategories/type.d.tsx` +- Quiz categories may require additional validation logic + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; + +export async function createQuizCRCategory(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` +```` + +## File: QuizCRCategories/Create.tsx +````typescript +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import { CreateFormProps } from '@/components/dashboard/cr/categories/type'; + +export default function createQuizCRCategory(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).create(data); +} +```` + +## File: QuizCRCategories/Delete.tsx +````typescript +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizCRCategories(id: string): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).delete(id); +} +```` + +## File: QuizCRCategories/GetAll.tsx +````typescript +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import { pb } from '@/lib/pb'; + +export default function getAllQuizCRCategories(): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).getFullList(); +} +```` + +## File: QuizCRCategories/GetAllCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_CR_CATEGORIES).getList(1, 9999, {}); + return count; +} +```` + +## File: QuizCRCategories/GetById.tsx +````typescript +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizCRCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).getOne(id); +} +```` + +## File: QuizCRCategories/GetHiddenCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizCRCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CR_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizCRCategories/GetVisibleCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizCRCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CR_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizCRCategories/Update.tsx +````typescript +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/cr/categories/type'; + +export default function updateQuizCRCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).update(id, data); +} +```` + +## File: QuizCRQuestions/Create.tsx +````typescript +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/cr/questions/type'; + +// interface CreateForm { +// // TODO: Add QuizCRQuestions fields +// } + +export default function createQuizCRQuestion(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_CR_QUESTIONS).create(data); +} +```` + +## File: QuizCRQuestions/Delete.tsx +````typescript +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizCRQuestions(id: string): Promise { + return pb.collection(COL_QUIZ_CR_QUESTIONS).delete(id); +} +```` + +## File: QuizCRQuestions/GetAll.tsx +````typescript +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizCRQuestions(): Promise { + return pb.collection(COL_QUIZ_CR_QUESTIONS).getFullList(); +} +```` + +## File: QuizCRQuestions/GetAllCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_CR_QUESTIONS).getList(1, 9999, {}); + return count; +} +```` + +## File: QuizCRQuestions/GetHiddenCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizCRQuestionsCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CR_QUESTIONS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizCRQuestions/GetVisibleCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizCRQuestionsCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CR_QUESTIONS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizListenings/Delete.tsx +````typescript +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizLPCategories(id: string): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).delete(id); +} +```` + +## File: QuizListenings/GetAll.tsx +````typescript +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizListenings(): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getFullList(); +} +```` + +## File: QuizListenings/GetAllCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, {}); + return count; +} +```` + +## File: QuizListenings/GetById.tsx +````typescript +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizListeningById(id: string): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); +} +```` + +## File: QuizListenings/GetHiddenCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizListenings/GetVisibleCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizListenings/ListWithOption.tsx +````typescript +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { ListResult, RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface ListWithOptionParams { + currentPage: number; + rowsPerPage: number; + listOption?: { + filter?: string; + sort?: string; + expand?: string; + }; +} + +export default function listWithOption({ + currentPage, + rowsPerPage, + listOption = {}, +}: ListWithOptionParams): Promise> { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getList(currentPage + 1, rowsPerPage, listOption); +} +```` + +## File: QuizLPCategories/_GUIDELINES.md +````markdown +# GUIDELINES + +This folder contains drivers for `QuizLPCategory` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/QuizLPCategories/type.d.tsx` +- Quiz LP categories may require additional validation logic + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +export async function createQuizLPCategory(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(COL_QUIZ_LP_CATEGORIES)) +} +``` +```` + +## File: QuizLPCategories/Create.tsx +````typescript +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function createQuizLPCategory(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).create(data); +} +```` + +## File: QuizLPCategories/Delete.tsx +````typescript +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizLPCategories(id: string): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).delete(id); +} +```` + +## File: QuizLPCategories/GetAll.tsx +````typescript +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizLPCategories(): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getFullList(); +} +```` + +## File: QuizLPCategories/GetAllCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, {}); + return count; +} +```` + +## File: QuizLPCategories/GetById.tsx +````typescript +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizLPCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); +} +```` + +## File: QuizLPCategories/GetHiddenCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizLPCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizLPCategories/GetVisibleCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizLPCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizLPCategories/Update.tsx +````typescript +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function updateQuizLPCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).update(id, data); +} +```` + +## File: QuizLPQuestions/_GUIDELINES.md +````markdown +# GUIDELINES + +This folder contains drivers for `QuizLPQuestion` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/QuizLPQuestions/type.d.tsx` +- Quiz LP questions require special handling for: + - Answer validation + - Question type checking + - Category association + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +export async function createQuizLPQuestion(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(COL_QUIZ_LP_QUESTIONS)) +} +``` +```` + +## File: QuizLPQuestions/_PROMPT.md +````markdown +please help to review the `tsx` file in this folder +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPQuestions` + +it was clone from +`LPCategories` +please help to modify to +`LPQuestions` + +please also help to modify the name of +`variables`, `constants`, `functions`, `classes`, components's name, paths + +the db fields structures between them are the same + +do not move the files +do not create directories +keep current folder structure is important + +thanks +```` + +## File: QuizLPQuestions/Create.tsx +````typescript +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/questions/type'; + +// interface CreateForm { +// // TODO: Add QuizLPQuestions fields +// } + +export default function createQuizLPQuestion(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).create(data); +} +```` + +## File: QuizLPQuestions/Delete.tsx +````typescript +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizLPQuestions(id: string): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).delete(id); +} +```` + +## File: QuizLPQuestions/GetAll.tsx +````typescript +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizLPQuestions(): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).getFullList(); +} +```` + +## File: QuizLPQuestions/GetAllCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_LP_QUESTIONS).getList(1, 9999, {}); + return count; +} +```` + +## File: QuizLPQuestions/GetById.tsx +````typescript +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizLPQuestionById(id: string): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).getOne(id); +} +```` + +## File: QuizLPQuestions/GetHiddenCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizLPQuestionsCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizLPQuestions/GetVisibleCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizLPQuestionsCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizLPQuestions/Update.tsx +````typescript +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function updateQuizLPCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).update(id, data); +} +```` + +## File: QuizMFCategories/Create.tsx +````typescript +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/mf/categories/type'; + +export default function createQuizMFCategory(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).create(data); +} +```` + +## File: QuizMFCategories/Delete.tsx +````typescript +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizMFCategories(id: string): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).delete(id); +} +```` + +## File: QuizMFCategories/GetAll.tsx +````typescript +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizMFCategories(): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).getFullList(); +} +```` + +## File: QuizMFCategories/GetAllCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_MF_CATEGORIES).getList(1, 9999, {}); + return count; +} +```` + +## File: QuizMFCategories/GetById.tsx +````typescript +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizMFCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).getOne(id); +} +```` + +## File: QuizMFCategories/GetHiddenCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizMFCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizMFCategories/GetVisibleCount.tsx +````typescript +// REQ0006 +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizMFCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: QuizMFCategories/Update.tsx +````typescript +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/mf/categories/type'; + +export default function updateQuizMFCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).update(id, data); +} +```` + +## File: Students/Create.tsx +````typescript +// api method for create student record +// RULES: +// TBA +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; +import type { CreateFormProps } from '@/components/dashboard/student/type.d'; +import type { RecordModel } from 'pocketbase'; + +export async function createStudent(data: CreateFormProps): Promise { + return pb.collection(COL_STUDENTS).create(data); +} +```` + +## File: Students/Delete.tsx +````typescript +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; + +export async function deleteStudent(id: string): Promise { + return pb.collection(COL_STUDENTS).delete(id); +} +```` + +## File: Students/GetActiveCount.tsx +````typescript +import { COL_STUDENTS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetActiveCount(): Promise { + const { totalItems: count } = await pb.collection(COL_STUDENTS).getList(1, 1, { + filter: 'status = "active"', + }); + return count; +} +```` + +## File: Students/GetAll.tsx +````typescript +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getAllStudents(options = {}): Promise { + return pb.collection(COL_STUDENTS).getFullList(options); +} +```` + +## File: Students/GetAllCount.tsx +````typescript +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; + +export async function getAllStudentsCount(): Promise { + const result = await pb.collection(COL_STUDENTS).getList(1, 1); + return result.totalItems; +} +```` + +## File: Students/GetBlockedCount.tsx +````typescript +import { COL_STUDENTS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetBlockedCount(): Promise { + const { totalItems: count } = await pb.collection(COL_STUDENTS).getList(1, 1, { + filter: 'status = "blocked"', + }); + return count; +} +```` + +## File: Students/GetById.tsx +````typescript +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getStudentById(id: string): Promise { + return pb.collection(COL_STUDENTS).getOne(id); +} +```` + +## File: Students/Update.tsx +````typescript +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import type { EditFormProps } from '@/components/dashboard/student/type.d'; + +export async function updateStudent(id: string, data: Partial): Promise { + return pb.collection(COL_STUDENTS).update(id, data); +} +```` + +## File: Subscriptions/_GUIDELINES.md +````markdown +# GUIDELINES + +This folder contains drivers for `Subscription`/`Subscriptions` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Subscriptions/type.d.tsx` +- Subscription records require special handling for: + - Payment status validation + - Expiration date checks + - Auto-renewal logic + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createSubscription(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Subscriptions')) +} +``` +```` + +## File: Subscriptions/Create.tsx.draft +```` +import { COL_SUBSCRIPTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/subscription/types'; + +export default function createSubscription(data: CreateForm): Promise { + return pb.collection(COL_SUBSCRIPTIONS).create(data); +} +```` + +## File: Subscriptions/Delete.tsx.draft +```` +import { COL_SUBSCRIPTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteSubscription(id: string): Promise { + // TODO: Add validation for active subscriptions if needed + return pb.collection(COL_SUBSCRIPTIONS).delete(id); +} +```` + +## File: Subscriptions/GetAll.tsx.draft +```` +import { COL_SUBSCRIPTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllSubscriptions(): Promise { + return pb.collection(COL_SUBSCRIPTIONS).getFullList({ + expand: 'user_id,plan_id', // Expand related user and plan data + sort: '-created' // Sort by most recent first + }); +} +```` + +## File: Subscriptions/GetAllCount.tsx.draft +```` +import { COL_SUBSCRIPTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getAllSubscriptionsCount(status?: string): Promise { + const filter = status ? `status = "${status}"` : ''; + const { totalItems: count } = await pb + .collection(COL_SUBSCRIPTIONS) + .getList(1, 9999, { filter }); + return count; +} +```` + +## File: Subscriptions/GetById.tsx.draft +```` +import { COL_SUBSCRIPTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getSubscriptionById(id: string): Promise { + return pb.collection(COL_SUBSCRIPTIONS).getOne(id, { + expand: 'user_id,plan_id' // Expand related user and plan data + }); +} +```` + +## File: Subscriptions/GetHiddenCount.tsx.draft +```` +// REQ0006 +import { COL_SUBSCRIPTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_SUBSCRIPTIONS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: Subscriptions/GetVisibleCount.tsx.draft +```` +// REQ0006 +import { COL_SUBSCRIPTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_SUBSCRIPTIONS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} +```` + +## File: Subscriptions/Update.tsx.draft +```` +import { COL_SUBSCRIPTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { UpdateForm } from '@/components/dashboard/subscription/types'; + +export default function updateSubscription(id: string, data: UpdateForm): Promise { + return pb.collection(COL_SUBSCRIPTIONS).update(id, data); +} +```` + +## File: Teachers/_GUIDELINES.md +````markdown +# GUIDELINES + +This folder contains drivers for `Teacher`/`Teachers` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Teachers/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createTeacher(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Teachers')) +} +``` +```` + +## File: Teachers/Create.tsx +````typescript +// api method for create teacher record +// RULES: +// TBA +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; +import type { CreateFormProps } from '@/components/dashboard/teacher/type.d'; +import type { RecordModel } from 'pocketbase'; + +export async function createTeacher(data: CreateFormProps): Promise { + return pb.collection(COL_TEACHERS).create(data); +} +```` + +## File: Teachers/GetActiveCount.tsx +````typescript +import { COL_TEACHERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetActiveCount(): Promise { + const { totalItems: count } = await pb.collection(COL_TEACHERS).getList(1, 1, { + filter: 'status = "active"', + }); + return count; +} +```` + +## File: Teachers/GetAll.tsx +````typescript +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getAllTeachers(options = {}): Promise { + return pb.collection(COL_TEACHERS).getFullList(options); +} +```` + +## File: Teachers/GetAllCount.tsx +````typescript +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; + +export async function getAllTeachersCount(): Promise { + const result = await pb.collection(COL_TEACHERS).getList(1, 1); + return result.totalItems; +} +```` + +## File: Teachers/GetBlockedCount.tsx +````typescript +import { COL_TEACHERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetBlockedCount(): Promise { + const { totalItems: count } = await pb.collection(COL_TEACHERS).getList(1, 1, { + filter: 'status = "blocked"', + }); + return count; +} +```` + +## File: Teachers/GetById.tsx +````typescript +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getTeacherById(id: string): Promise { + return pb.collection(COL_TEACHERS).getOne(id); +} +```` + +## File: Teachers/GetPendingCount.tsx +````typescript +import { COL_TEACHERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetPendingCount(): Promise { + const { totalItems: count } = await pb.collection(COL_TEACHERS).getList(1, 1, { + filter: 'status = "pending"', + }); + return count; +} +```` + +## File: Teachers/Update.tsx +````typescript +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import type { EditFormProps } from '@/components/dashboard/teacher/type.d'; + +export async function updateTeacher(id: string, data: Partial): Promise { + return pb.collection(COL_TEACHERS).update(id, data); +} +```` + +## File: UserMetas/_GUIDELINES.md +````markdown +# GUIDELINES + +This folder contains drivers for `UserMeta`/`UserMetas` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/UserMetas/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_USER_METAS } from '@/constants'; + +export async function createUserMeta(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` +```` + +## File: UserMetas/Create.tsx.draft +```` +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/lesson_type/types'; + +// import type { CreateForm } from '@/components/dashboard/lesson_type/interfaces.ts.del'; + +export default function createLessonType(data: CreateForm): Promise { + return pb.collection(COL_LESSON_TYPES).create(data); +} +```` + +## File: UserMetas/Delete.tsx.draft +```` +import { COL_USER_METAS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default function deleteUserMeta(id: string): Promise { + return pb.collection(COL_USER_METAS).delete(id); +} +```` + +## File: UserMetas/GetAll.tsx.draft +```` +import { COL_USER_METAS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import { pb } from '@/lib/pb'; + +export default function getAllUserMetas(): Promise { + return pb.collection(COL_USER_METAS).getFullList(); +} +```` + +## File: UserMetas/GetAllCount.tsx +````typescript +// RULES: +// error handled by caller +// contain definition to collection only + +import { COL_USER_METAS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function getAllUserMetasCount(): Promise { + return pb + .collection(COL_USER_METAS) + .getList(1, 9998) + .then((res) => res.totalItems); +} +```` + +## File: UserMetas/GetById.tsx.draft +```` +import { COL_USER_METAS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import { pb } from '@/lib/pb'; + +export default function getUserMetaById(id: string): Promise { + return pb.collection(COL_USER_METAS).getOne(id); +} +```` + +## File: UserMetas/Update.tsx.draft +```` +import { COL_USER_METAS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/user_meta/types'; + +export default function updateUserMeta(id: string, data: CreateForm): Promise { + return pb.collection(COL_USER_METAS).update(id, data); +} +```` + +## File: Users/_GUIDELINES.md +````markdown +# GUIDELINES + +This folder contains drivers for `User`/`Users` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Users/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_USERS } from '@/constants'; + +export async function createUser(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` +```` + +## File: Users/Create.tsx.draft +```` +import { COL_USERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/user/types'; + + +export default function createUser(data: CreateForm): Promise { + return pb.collection(COL_USERS).create(data); +} +```` + +## File: Users/Delete.tsx.draft +```` +import { COL_USERS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteUser(id: string): Promise { + return pb.collection(COL_USERS).delete(id); +} +```` + +## File: Users/GetAll.tsx.draft +```` +import { COL_USERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllUsers(): Promise { + return pb.collection(COL_USERS).getFullList(); +} +```` + +## File: Users/GetAllCount.tsx +````typescript +// REQ0006 +import { COL_USERS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + try { + const result = await pb.collection(`users`).getList(1, 9999, { filter: 'email != ""' }); + const { totalItems: count } = result; + return count; + } catch (error) { + console.error(error); + return -99; + } +} +```` + +## File: Users/GetById.tsx.draft +```` +import { COL_USERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getUserById(id: string): Promise { + return pb.collection(COL_USERS).getOne(id); +} +```` + +## File: Users/Update.tsx.draft +```` +import { COL_USERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/user/types'; + +export default function updateUser(id: string, data: CreateForm): Promise { + return pb.collection(COL_USERS).update(id, data); +} +```` + +## File: DB_AI_GUIDELINE.MD +````markdown +# AI GUIDELINE + +## getting started + +Imagine there is a software developer and a QA engineer to solve the problems together + +They will: + +no need to reply me what you are going on and your digest in this phase. +just reply me "OK" when done + +base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project` + +- read `/001_documentation/Requirements/REQ0006/schema.dbml` +this is file in dbml syntax state the main database + +- read `/002_source/cms/src/db/schema.json` +this is the file of live pocketbase schema output + +- read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts` +this is the content of `@/constants` + +- look into the md files in folder `/002_source/cms/_AI_GUIDELINE` + +- read, remember and link up the ideas in file stated above, +i will tell them the task afterwards + +--- + +The software engineer will provide solutions, +while QA engineer will feedback the opinion. + +this is now not in debug phase, +so, no need to reply me what they are going on or their insight throught the prompt. +just reply me "OK" when done + +--- + +clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it + +please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden` + +well done !, please proceed to another request + +working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db` + +according information from `schema.json`, get the collection of `Students` + +pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content + +when you draft coding, review file and append with `.tsx.draft` + +--- + +- this is part of react typescript project, with pocketbase +- `schema.dbml`, describe the collections(tables) +- folder `LessonCategories`, the correct references +- folder `LessonTypes`, the correct references +- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006` +- do not read root directory, assume it is a fresh copy of nextjs project is ok + +## instruction + +- break the questions into smaller parts +- review file append with `.draft`, see if the content aligned with the correct references +- read and understand `dbml` file +- lookup the every folder + +## tasks + +Thanks + + + +--- + + +please take a look in `schema.dbml` and `schema.json`, +associate the collection from json file to the table in dbml file + +please modify the `schema.dbml` to align with `schema.json` + +to the collection `QuizLPCategories` align the dbml file in the previous prompt + + +--- + +please revise + +please revise +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory` + +to the collection `QuizLPCategories` align the dbml file in the previous prompt + + +please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx` + +to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory` + +--- + +the constants file (`@/constants`) was `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts` + +please help to fix the `tsx` files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizMFCategories`, +the `COL` constants is wrongly used, it should refer to `COL_QUIZ_MF_CATEGORIES`. thanks + + +please update the `COL_XXXX` TO COL_MF_CATEGORIES +```` + +## File: repomix-output.xml +````xml +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) + + + + + + + + + +_PROMPT/ + 1.MD + 2.MD + 3.MD + 4.md + temp.md +Customers/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetActiveCount.tsx + GetAll.tsx + GetAllCount.tsx + GetBlockedCount.tsx + GetById.tsx + GetPendingCount.tsx + Helloworld.tsx + Update.tsx +Events/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Update.tsx.draft +Helloworlds/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Helloworld.tsx.draft + Update.tsx.draft +LessonCategories/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Helloworld.tsx + Update.tsx +LessonTypes/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Helloworld.tsx + Update.tsx +Messages/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Update.tsx.draft +QuizCategories/ + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Helloworld.tsx.draft + Update.tsx.draft +QuizConnectives/ + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Helloworld.tsx.draft + Update.tsx.draft +QuizConnectivesCategories/ + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Helloworld.tsx.draft + Update.tsx.draft +QuizCRCategories/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Update.tsx +QuizCRQuestions/ + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx +QuizListenings/ + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + ListWithOption.tsx +QuizLPCategories/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Update.tsx +QuizLPQuestions/ + _GUIDELINES.md + _PROMPT.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Update.tsx +QuizMFCategories/ + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Update.tsx +Students/ + Create.tsx + Delete.tsx + GetActiveCount.tsx + GetAll.tsx + GetAllCount.tsx + GetBlockedCount.tsx + GetById.tsx + Update.tsx +Subscriptions/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Update.tsx.draft +Teachers/ + _GUIDELINES.md + Create.tsx + GetActiveCount.tsx + GetAll.tsx + GetAllCount.tsx + GetBlockedCount.tsx + GetById.tsx + GetPendingCount.tsx + Update.tsx +UserMetas/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx + GetById.tsx.draft + Update.tsx.draft +Users/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx + GetById.tsx.draft + Update.tsx.draft +DB_AI_GUIDELINE.MD +schema.json + + + +This section contains the contents of the repository's files. + + +Hi, please study the documentation below, +i will send you the task afterwards, + +please read and understand the documentation below and link up the ideas +reply `OK` when you done +no need to state me any other things, thanks + +1. `schema.dbml` + +- this describe the database schema in dbml format +- filepath: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml` + +2. `schema.json` + +- this is the schema export in pocketbase format +- filepath: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/schema.json` + +3. `_AI_GUIDELINE`: + +- there are the markdown files that help you better understand the implementation +- directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/_AI_GUIDELINE` + +thanks + +--- + +# task + +clone from `LessonTypes` to `Customers` + +## steps + +1. read `tsx` files from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/LessonTypes` +1. copy file to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers` +1. modify the copied `tsx` files to suit `customer` fields + + + +update `LpCategoryDefaultValue` +in file `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.ts` + +thanks + +you can find the type def in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` + +please help to draft code file: + +base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db` + +using +`$base_dir/QuizListenings/GetHiddenCount.tsx`, +`$base_dir/QuizListenings/GetVisibleCount.tsx`, +`$base_dir/LessonTypes/GetHiddenCount.tsx`, +`$base_dir/LessonTypes/GetVisibleCount.tsx`, +as reference, + +look into the all directories under base_dir e.g. `QuizCategories`. +propergate `GetHiddenCount.tsx` and `GetVisibleCount.tsx` if missing, do the change to suit the collection. +use `.draft.tsx` instead when you write file + +--- + +rewrite `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/LessonCategories/GetAllCount.tsx` to match `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/UserMetas/GetAllCount.tsx` style + +--- + +style rewrite + +study +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/overview/summary/ActiveUserCount/index.tsx` +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/overview/summary/LessonCategoriesCount/index.tsx` + +and rewrite `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/overview/summary/LessonTypeCount/index.tsx` to match style above thanks + + + +please draft with idea: + +``` +await pb +.collection(COL_LESSON_TYPES) +.getList(currentPage + 1, rowsPerPage, listOption); +``` + +for Listening Practice + +thanks + +I want you to clone +from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/LessonTypes/GetVisibleCount.tsx` (source file) + +to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizListenings/GetVisibleCount.tsx` (dest file) + +please extract , link up and remember the document properties +(e.g. types, functions, variables, constants, etc) +from source file +draft dest file + +update the variables and properties of dest file to reflect `listening practice categories`/`lp_categories` + +--- + +## task + +update `schema.dbml` to reflect `schema.json` + +## details + +Hi, +I have a pocketbase export json file: +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/pocketbase/pb_hooks/seed/schema.json` + +and a dbml file: +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml` + +the collection name in pocketbase should be reflected by a table in dbml, + +## steps + +compare `schema.json` and `schema.dbml` +please keep `schema.json` remain unchanged +update `schema.dbml` to reflect `schema.json` +do check again when finished + + + +--- + +clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it + +please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden` + +well done !, please proceed to another request + +working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db` + +according information from `schema.json`, get the collection of `Students` + +pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content + +when you draft coding, review file and append with `.tsx.draft` + +--- + +- this is part of react typescript project, with pocketbase +- `schema.dbml`, describe the collections(tables) +- folder `LessonCategories`, the correct references +- folder `LessonTypes`, the correct references +- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006` +- do not read root directory, assume it is a fresh copy of nextjs project is ok + +## instruction + +- break the questions into smaller parts +- review file append with `.draft`, see if the content aligned with the correct references +- read and understand `dbml` file +- lookup the every folder + +## tasks + +Thanks + +--- + +please take a look in `schema.dbml` and `schema.json`, +associate the collection from json file to the table in dbml file + +please modify the `schema.dbml` to align with `schema.json` + +to the collection `QuizLPCategories` align the dbml file in the previous prompt + +--- + +please revise + +please revise +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory` + +to the collection `QuizLPCategories` align the dbml file in the previous prompt + +please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx` + +to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory` + + + +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/lp-categories-filters.tsx` + +this file is original for `lesson_category` model, +please modify it to fit `lp_category` (listening practice category) + +thanks + + + +# GUIDELINES + +This folder contains drivers for `Customer`/`Customers` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx, GetActiveCount.tsx, GetBlockedCount.tsx, GetPendingCount.tsx) +- misc (Helloworld.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; + +export async function createCustomer(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` + + + +// api method for crate customer record +// RULES: +// TBA +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; +import type { CreateFormProps } from '@/components/dashboard/customer/type.d'; +import type { RecordModel } from 'pocketbase'; + +export async function createCustomer(data: CreateFormProps): Promise { + return pb.collection(COL_CUSTOMERS).create(data); +} + + + +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; + +export async function deleteCustomer(id: string): Promise { + return pb.collection(COL_CUSTOMERS).delete(id); +} + + + +import { COL_CUSTOMERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetActiveCount(): Promise { + const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, { + filter: 'status = "active"', + }); + return count; +} + + + +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getAllCustomers(options = {}): Promise { + return pb.collection(COL_CUSTOMERS).getFullList(options); +} + + + +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; + +export async function getAllCustomersCount(): Promise { + const result = await pb.collection(COL_CUSTOMERS).getList(1, 1); + return result.totalItems; +} + + + +import { COL_CUSTOMERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetBlockedCount(): Promise { + const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, { + filter: 'status = "blocked"', + }); + return count; +} + + + +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getCustomerById(id: string): Promise { + return pb.collection(COL_CUSTOMERS).getOne(id); +} + + + +import { COL_CUSTOMERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetPendingCount(): Promise { + const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, { + filter: 'status = "pending"', + }); + return count; +} + + + +export function helloCustomer() { + return 'Hello from Customers module!'; +} + + + +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import type { EditFormProps } from '@/components/dashboard/customer/type.d'; + +export async function updateCustomer(id: string, data: Partial): Promise { + return pb.collection(COL_CUSTOMERS).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `Event`/`Events` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Events/type.d.tsx` +- Event records require special handling for: + - Date/time validation + - Location data + - Attendee management + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createEvent(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Events')) +} +``` + + + +import { COL_EVENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/event/types'; + +export default function createEvent(data: CreateForm): Promise { + return pb.collection(COL_EVENTS).create(data); +} + + + +import { COL_EVENTS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteEvent(id: string): Promise { + return pb.collection(COL_EVENTS).delete(id); +} + + + +import { COL_EVENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllEvents(): Promise { + return pb.collection(COL_EVENTS).getFullList({ + sort: 'event_time' // Sort by event time + }); +} + + + +import { COL_EVENTS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getAllEventsCount(startDate?: string, endDate?: string): Promise { + let filter = ''; + if (startDate && endDate) { + filter = `event_time >= "${startDate}" && event_time <= "${endDate}"`; + } else if (startDate) { + filter = `event_time >= "${startDate}"`; + } else if (endDate) { + filter = `event_time <= "${endDate}"`; + } + + const { totalItems: count } = await pb + .collection(COL_EVENTS) + .getList(1, 9999, { filter }); + return count; +} + + + +import { COL_EVENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getEventById(id: string): Promise { + return pb.collection(COL_EVENTS).getOne(id); +} + + + +// REQ0006 +import { COL_EVENTS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_EVENTS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_EVENTS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_EVENTS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_EVENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { UpdateForm } from '@/components/dashboard/event/types'; + +export default function updateEvent(id: string, data: UpdateForm): Promise { + return pb.collection(COL_EVENTS).update(id, data); +} + + + +# GUIDELINES + +This folder contains test drivers for `Helloworld` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function +- type information defined in `@/db/Helloworlds/type.d.tsx` +- This is a test collection - keep implementations simple + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createHelloworld(data: CreateFormProps) { + // Simple test implementation + return pb.collection('Helloworlds').create(data); +} +``` + + + +import { COL_HELLOWORLDS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/helloworld/types'; + +export default function createHelloworld(data: CreateForm): Promise { + return pb.collection(COL_HELLOWORLDS).create(data); +} + + + +import { COL_HELLOWORLDS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default function deleteHelloworld(id: string): Promise { + return pb.collection(COL_HELLOWORLDS).delete(id); +} + + + +import { COL_HELLOWORLDS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllHelloworlds(): Promise { + return pb.collection(COL_HELLOWORLDS).getFullList(); +} + + + +import { COL_HELLOWORLDS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getHelloworldById(id: string): Promise { + return pb.collection(COL_HELLOWORLDS).getOne(id); +} + + + +// REQ0006 +import { COL_HELLOWORLDS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_HELLOWORLDS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_HELLOWORLDS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_HELLOWORLDS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; + + + +import { COL_HELLOWORLDS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/helloworld/types'; + +export default function updateHelloworld(id: string, data: CreateForm): Promise { + return pb.collection(COL_HELLOWORLDS).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `LessonCategory`/`LessonCategories` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/LessonCategories/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_LESSON_CATEGORIES } from '@/constants'; + +export async function createLessonCategory(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` + + + +import { COL_LESSON_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function createLessonCategory(data: CreateFormProps): Promise { + return pb.collection(COL_LESSON_CATEGORIES).create(data); +} + + + +import { COL_LESSON_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteLessonCategory(id: string): Promise { + return pb.collection(COL_LESSON_CATEGORIES).delete(id); +} + + + +import { COL_LESSON_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllLessonCategories(): Promise { + return pb.collection(COL_LESSON_CATEGORIES).getFullList(); +} + + + +// RULES: +// error handled by caller +// contain definition to collection only + +import { COL_LESSON_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function getAllLessonCategoriesCount(): Promise { + return pb + .collection(COL_LESSON_CATEGORIES) + .getList(1, 9999) + .then((res) => res.totalItems); +} + + + +import { COL_LESSON_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getLessonCategoryById(id: string): Promise { + return pb.collection(COL_LESSON_CATEGORIES).getOne(id); +} + + + +// REQ0006 +import { COL_LESSON_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_LESSON_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; + + + +import { COL_LESSON_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function updateLessonCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_LESSON_CATEGORIES).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `LessonType`/`LessonTypes` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/LessonTypes/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_LESSON_TYPES } from '@/constants'; + +export async function createLessonType(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` + + + +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/lesson_type/lesson-type'; + +export default function createLessonType(data: CreateForm): Promise { + return pb.collection(COL_LESSON_TYPES).create(data); +} + + + +import { COL_LESSON_TYPES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteLessonType(id: string): Promise { + return pb.collection(COL_LESSON_TYPES).delete(id); +} + + + +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllLessonTypes(): Promise { + return pb.collection(COL_LESSON_TYPES).getFullList(); +} + + + +// REQ0006 +import { COL_LESSON_TYPES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_TYPES).getList(1, 9999, {}); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getLessonTypeById(id: string): Promise { + return pb.collection(COL_LESSON_TYPES).getOne(id); +} + + + +// REQ0006 +import { COL_LESSON_TYPES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_TYPES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_LESSON_TYPES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_TYPES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; + + + +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/lesson_type/lesson-type'; + +export default function updateLessonType(id: string, data: CreateForm): Promise { + return pb.collection(COL_LESSON_TYPES).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `Message`/`Messages` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Messages/type.d.tsx` +- Message records require special handling for: + - Sender/receiver validation + - Timestamp management + - Read status tracking + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createMessage(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Messages')) +} +``` + + + +import { COL_MESSAGES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/message/types'; + +export default function createMessage(data: CreateForm): Promise { + return pb.collection(COL_MESSAGES).create(data); +} + + + +import { COL_MESSAGES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteMessage(id: string): Promise { + return pb.collection(COL_MESSAGES).delete(id); +} + + + +import { COL_MESSAGES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllMessages(): Promise { + return pb.collection(COL_MESSAGES).getFullList({ + expand: 'user_id', // Expand related user data + sort: '-created' // Sort by most recent first + }); +} + + + +import { COL_MESSAGES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getAllMessagesCount(status?: string): Promise { + const filter = status ? `status = "${status}"` : ''; + const { totalItems: count } = await pb + .collection(COL_MESSAGES) + .getList(1, 9999, { filter }); + return count; +} + + + +import { COL_MESSAGES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getMessageById(id: string): Promise { + return pb.collection(COL_MESSAGES).getOne(id, { + expand: 'user_id' // Expand related user data + }); +} + + + +// REQ0006 +import { COL_MESSAGES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_MESSAGES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_MESSAGES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_MESSAGES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_MESSAGES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { UpdateForm } from '@/components/dashboard/message/types'; + +export default function updateMessage(id: string, data: UpdateForm): Promise { + return pb.collection(COL_MESSAGES).update(id, data); +} + + + +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface CreateForm { + // TODO: Add QuizCategories fields +} + +export default function createQuizCategory(data: CreateForm): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).create(data); +} + + + +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizCategory(id: string): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).delete(id); +} + + + +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizCategories(): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).getFullList(); +} + + + +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; + + + +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface UpdateForm { + // TODO: Add QuizCategories fields +} + +export default function updateQuizCategory(id: string, data: UpdateForm): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).update(id, data); +} + + + +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface CreateForm { + // TODO: Add QuizConnectives fields +} + +export default function createQuizConnective(data: CreateForm): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).create(data); +} + + + +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizConnective(id: string): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).delete(id); +} + + + +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizConnectives(): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).getFullList(); +} + + + +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizConnectiveById(id: string): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_CONNECTIVES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CONNECTIVES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_CONNECTIVES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CONNECTIVES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; + + + +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface UpdateForm { + // TODO: Add QuizConnectives fields +} + +export default function updateQuizConnective(id: string, data: UpdateForm): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).update(id, data); +} + + + +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface CreateForm { + // TODO: Add QuizConnectivesCategories fields +} + +export default function createQuizConnectivesCategory(data: CreateForm): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).create(data); +} + + + +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizConnectivesCategory(id: string): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).delete(id); +} + + + +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizConnectivesCategories(): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).getFullList(); +} + + + +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizConnectivesCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb + .collection(COL_QUIZ_CONNECTIVES_CATEGORIES) + .getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb + .collection(COL_QUIZ_CONNECTIVES_CATEGORIES) + .getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; + + + +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface UpdateForm { + // TODO: Add QuizConnectivesCategories fields +} + +export default function updateQuizConnectivesCategory(id: string, data: UpdateForm): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `QuizCRCategory`/`QuizCRCategories` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/QuizCRCategories/type.d.tsx` +- Quiz categories may require additional validation logic + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; + +export async function createQuizCRCategory(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` + + + +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import { CreateFormProps } from '@/components/dashboard/cr/categories/type'; + +export default function createQuizCRCategory(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).create(data); +} + + + +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizCRCategories(id: string): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).delete(id); +} + + + +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import { pb } from '@/lib/pb'; + +export default function getAllQuizCRCategories(): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).getFullList(); +} + + + +// REQ0006 +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_CR_CATEGORIES).getList(1, 9999, {}); + return count; +} + + + +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizCRCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizCRCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CR_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizCRCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CR_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/cr/categories/type'; + +export default function updateQuizCRCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).update(id, data); +} + + + +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/cr/questions/type'; + +// interface CreateForm { +// // TODO: Add QuizCRQuestions fields +// } + +export default function createQuizCRQuestion(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_CR_QUESTIONS).create(data); +} + + + +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizCRQuestions(id: string): Promise { + return pb.collection(COL_QUIZ_CR_QUESTIONS).delete(id); +} + + + +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizCRQuestions(): Promise { + return pb.collection(COL_QUIZ_CR_QUESTIONS).getFullList(); +} + + + +// REQ0006 +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_CR_QUESTIONS).getList(1, 9999, {}); + return count; +} + + + +// REQ0006 +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizCRQuestionsCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CR_QUESTIONS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizCRQuestionsCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CR_QUESTIONS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizLPCategories(id: string): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).delete(id); +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizListenings(): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getFullList(); +} + + + +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, {}); + return count; +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizListeningById(id: string): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { ListResult, RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface ListWithOptionParams { + currentPage: number; + rowsPerPage: number; + listOption?: { + filter?: string; + sort?: string; + expand?: string; + }; +} + +export default function listWithOption({ + currentPage, + rowsPerPage, + listOption = {}, +}: ListWithOptionParams): Promise> { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getList(currentPage + 1, rowsPerPage, listOption); +} + + + +# GUIDELINES + +This folder contains drivers for `QuizLPCategory` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/QuizLPCategories/type.d.tsx` +- Quiz LP categories may require additional validation logic + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +export async function createQuizLPCategory(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(COL_QUIZ_LP_CATEGORIES)) +} +``` + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function createQuizLPCategory(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).create(data); +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizLPCategories(id: string): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).delete(id); +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizLPCategories(): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getFullList(); +} + + + +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, {}); + return count; +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizLPCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizLPCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizLPCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function updateQuizLPCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `QuizLPQuestion` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/QuizLPQuestions/type.d.tsx` +- Quiz LP questions require special handling for: + - Answer validation + - Question type checking + - Category association + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +export async function createQuizLPQuestion(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(COL_QUIZ_LP_QUESTIONS)) +} +``` + + + +please help to review the `tsx` file in this folder +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPQuestions` + +it was clone from +`LPCategories` +please help to modify to +`LPQuestions` + +please also help to modify the name of +`variables`, `constants`, `functions`, `classes`, components's name, paths + +the db fields structures between them are the same + +do not move the files +do not create directories +keep current folder structure is important + +thanks + + + +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/questions/type'; + +// interface CreateForm { +// // TODO: Add QuizLPQuestions fields +// } + +export default function createQuizLPQuestion(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).create(data); +} + + + +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizLPQuestions(id: string): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).delete(id); +} + + + +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizLPQuestions(): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).getFullList(); +} + + + +// REQ0006 +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_LP_QUESTIONS).getList(1, 9999, {}); + return count; +} + + + +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizLPQuestionById(id: string): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizLPQuestionsCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizLPQuestionsCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function updateQuizLPCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).update(id, data); +} + + + +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/mf/categories/type'; + +export default function createQuizMFCategory(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).create(data); +} + + + +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizMFCategories(id: string): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).delete(id); +} + + + +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizMFCategories(): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).getFullList(); +} + + + +// REQ0006 +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_MF_CATEGORIES).getList(1, 9999, {}); + return count; +} + + + +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizMFCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizMFCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizMFCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/mf/categories/type'; + +export default function updateQuizMFCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).update(id, data); +} + + + +// api method for create student record +// RULES: +// TBA +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; +import type { CreateFormProps } from '@/components/dashboard/student/type.d'; +import type { RecordModel } from 'pocketbase'; + +export async function createStudent(data: CreateFormProps): Promise { + return pb.collection(COL_STUDENTS).create(data); +} + + + +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; + +export async function deleteStudent(id: string): Promise { + return pb.collection(COL_STUDENTS).delete(id); +} + + + +import { COL_STUDENTS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetActiveCount(): Promise { + const { totalItems: count } = await pb.collection(COL_STUDENTS).getList(1, 1, { + filter: 'status = "active"', + }); + return count; +} + + + +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getAllStudents(options = {}): Promise { + return pb.collection(COL_STUDENTS).getFullList(options); +} + + + +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; + +export async function getAllStudentsCount(): Promise { + const result = await pb.collection(COL_STUDENTS).getList(1, 1); + return result.totalItems; +} + + + +import { COL_STUDENTS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetBlockedCount(): Promise { + const { totalItems: count } = await pb.collection(COL_STUDENTS).getList(1, 1, { + filter: 'status = "blocked"', + }); + return count; +} + + + +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getStudentById(id: string): Promise { + return pb.collection(COL_STUDENTS).getOne(id); +} + + + +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import type { EditFormProps } from '@/components/dashboard/student/type.d'; + +export async function updateStudent(id: string, data: Partial): Promise { + return pb.collection(COL_STUDENTS).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `Subscription`/`Subscriptions` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Subscriptions/type.d.tsx` +- Subscription records require special handling for: + - Payment status validation + - Expiration date checks + - Auto-renewal logic + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createSubscription(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Subscriptions')) +} +``` + + + +import { COL_SUBSCRIPTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/subscription/types'; + +export default function createSubscription(data: CreateForm): Promise { + return pb.collection(COL_SUBSCRIPTIONS).create(data); +} + + + +import { COL_SUBSCRIPTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteSubscription(id: string): Promise { + // TODO: Add validation for active subscriptions if needed + return pb.collection(COL_SUBSCRIPTIONS).delete(id); +} + + + +import { COL_SUBSCRIPTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllSubscriptions(): Promise { + return pb.collection(COL_SUBSCRIPTIONS).getFullList({ + expand: 'user_id,plan_id', // Expand related user and plan data + sort: '-created' // Sort by most recent first + }); +} + + + +import { COL_SUBSCRIPTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getAllSubscriptionsCount(status?: string): Promise { + const filter = status ? `status = "${status}"` : ''; + const { totalItems: count } = await pb + .collection(COL_SUBSCRIPTIONS) + .getList(1, 9999, { filter }); + return count; +} + + + +import { COL_SUBSCRIPTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getSubscriptionById(id: string): Promise { + return pb.collection(COL_SUBSCRIPTIONS).getOne(id, { + expand: 'user_id,plan_id' // Expand related user and plan data + }); +} + + + +// REQ0006 +import { COL_SUBSCRIPTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_SUBSCRIPTIONS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_SUBSCRIPTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_SUBSCRIPTIONS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_SUBSCRIPTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { UpdateForm } from '@/components/dashboard/subscription/types'; + +export default function updateSubscription(id: string, data: UpdateForm): Promise { + return pb.collection(COL_SUBSCRIPTIONS).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `Teacher`/`Teachers` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Teachers/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createTeacher(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Teachers')) +} +``` + + + +// api method for create teacher record +// RULES: +// TBA +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; +import type { CreateFormProps } from '@/components/dashboard/teacher/type.d'; +import type { RecordModel } from 'pocketbase'; + +export async function createTeacher(data: CreateFormProps): Promise { + return pb.collection(COL_TEACHERS).create(data); +} + + + +import { COL_TEACHERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetActiveCount(): Promise { + const { totalItems: count } = await pb.collection(COL_TEACHERS).getList(1, 1, { + filter: 'status = "active"', + }); + return count; +} + + + +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getAllTeachers(options = {}): Promise { + return pb.collection(COL_TEACHERS).getFullList(options); +} + + + +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; + +export async function getAllTeachersCount(): Promise { + const result = await pb.collection(COL_TEACHERS).getList(1, 1); + return result.totalItems; +} + + + +import { COL_TEACHERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetBlockedCount(): Promise { + const { totalItems: count } = await pb.collection(COL_TEACHERS).getList(1, 1, { + filter: 'status = "blocked"', + }); + return count; +} + + + +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getTeacherById(id: string): Promise { + return pb.collection(COL_TEACHERS).getOne(id); +} + + + +import { COL_TEACHERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetPendingCount(): Promise { + const { totalItems: count } = await pb.collection(COL_TEACHERS).getList(1, 1, { + filter: 'status = "pending"', + }); + return count; +} + + + +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import type { EditFormProps } from '@/components/dashboard/teacher/type.d'; + +export async function updateTeacher(id: string, data: Partial): Promise { + return pb.collection(COL_TEACHERS).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `UserMeta`/`UserMetas` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/UserMetas/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_USER_METAS } from '@/constants'; + +export async function createUserMeta(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` + + + +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/lesson_type/types'; + +// import type { CreateForm } from '@/components/dashboard/lesson_type/interfaces.ts.del'; + +export default function createLessonType(data: CreateForm): Promise { + return pb.collection(COL_LESSON_TYPES).create(data); +} + + + +import { COL_USER_METAS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default function deleteUserMeta(id: string): Promise { + return pb.collection(COL_USER_METAS).delete(id); +} + + + +import { COL_USER_METAS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import { pb } from '@/lib/pb'; + +export default function getAllUserMetas(): Promise { + return pb.collection(COL_USER_METAS).getFullList(); +} + + + +// RULES: +// error handled by caller +// contain definition to collection only + +import { COL_USER_METAS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function getAllUserMetasCount(): Promise { + return pb + .collection(COL_USER_METAS) + .getList(1, 9998) + .then((res) => res.totalItems); +} + + + +import { COL_USER_METAS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import { pb } from '@/lib/pb'; + +export default function getUserMetaById(id: string): Promise { + return pb.collection(COL_USER_METAS).getOne(id); +} + + + +import { COL_USER_METAS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/user_meta/types'; + +export default function updateUserMeta(id: string, data: CreateForm): Promise { + return pb.collection(COL_USER_METAS).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `User`/`Users` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Users/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_USERS } from '@/constants'; + +export async function createUser(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` + + + +import { COL_USERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/user/types'; + + +export default function createUser(data: CreateForm): Promise { + return pb.collection(COL_USERS).create(data); +} + + + +import { COL_USERS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteUser(id: string): Promise { + return pb.collection(COL_USERS).delete(id); +} + + + +import { COL_USERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllUsers(): Promise { + return pb.collection(COL_USERS).getFullList(); +} + + + +// REQ0006 +import { COL_USERS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + try { + const result = await pb.collection(`users`).getList(1, 9999, { filter: 'email != ""' }); + const { totalItems: count } = result; + return count; + } catch (error) { + console.error(error); + return -99; + } +} + + + +import { COL_USERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getUserById(id: string): Promise { + return pb.collection(COL_USERS).getOne(id); +} + + + +import { COL_USERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/user/types'; + +export default function updateUser(id: string, data: CreateForm): Promise { + return pb.collection(COL_USERS).update(id, data); +} + + + +# AI GUIDELINE + +## getting started + +Imagine there is a software developer and a QA engineer to solve the problems together + +They will: + +no need to reply me what you are going on and your digest in this phase. +just reply me "OK" when done + +base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project` + +- read `/001_documentation/Requirements/REQ0006/schema.dbml` +this is file in dbml syntax state the main database + +- read `/002_source/cms/src/db/schema.json` +this is the file of live pocketbase schema output + +- read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts` +this is the content of `@/constants` + +- look into the md files in folder `/002_source/cms/_AI_GUIDELINE` + +- read, remember and link up the ideas in file stated above, +i will tell them the task afterwards + +--- + +The software engineer will provide solutions, +while QA engineer will feedback the opinion. + +this is now not in debug phase, +so, no need to reply me what they are going on or their insight throught the prompt. +just reply me "OK" when done + +--- + +clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it + +please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden` + +well done !, please proceed to another request + +working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db` + +according information from `schema.json`, get the collection of `Students` + +pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content + +when you draft coding, review file and append with `.tsx.draft` + +--- + +- this is part of react typescript project, with pocketbase +- `schema.dbml`, describe the collections(tables) +- folder `LessonCategories`, the correct references +- folder `LessonTypes`, the correct references +- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006` +- do not read root directory, assume it is a fresh copy of nextjs project is ok + +## instruction + +- break the questions into smaller parts +- review file append with `.draft`, see if the content aligned with the correct references +- read and understand `dbml` file +- lookup the every folder + +## tasks + +Thanks + + + +--- + + +please take a look in `schema.dbml` and `schema.json`, +associate the collection from json file to the table in dbml file + +please modify the `schema.dbml` to align with `schema.json` + +to the collection `QuizLPCategories` align the dbml file in the previous prompt + + +--- + +please revise + +please revise +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory` + +to the collection `QuizLPCategories` align the dbml file in the previous prompt + + +please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx` + +to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory` + +--- + +the constants file (`@/constants`) was `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts` + +please help to fix the `tsx` files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizMFCategories`, +the `COL` constants is wrongly used, it should refer to `COL_QUIZ_MF_CATEGORIES`. thanks + + +please update the `COL_XXXX` TO COL_MF_CATEGORIES + + + +[ + { + "id": "pbc_3142635823", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "_superusers", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_tokenKey_pbc_3142635823` ON `_superusers` (`tokenKey`)", + "CREATE UNIQUE INDEX `idx_email_pbc_3142635823` ON `_superusers` (`email`) WHERE `email` != ''" + ], + "system": true, + "authRule": "", + "manageRule": null, + "authAlert": { + "enabled": true, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "mappedFields": { + "id": "", + "name": "", + "username": "", + "avatarURL": "" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 86400 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 259200 + }, + "fileToken": { + "duration": 180 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + { + "id": "_pb_users_auth_", + "listRule": "id = @request.auth.id", + "viewRule": "id = @request.auth.id", + "createRule": "", + "updateRule": "id = @request.auth.id", + "deleteRule": "id = @request.auth.id", + "name": "users", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 255, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file376926767", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + "image/webp" + ], + "name": "avatar", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": null, + "type": "file" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_tokenKey__pb_users_auth_` ON `users` (`tokenKey`)", + "CREATE UNIQUE INDEX `idx_email__pb_users_auth_` ON `users` (`email`) WHERE `email` != ''" + ], + "system": false, + "authRule": "", + "manageRule": null, + "authAlert": { + "enabled": true, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "mappedFields": { + "id": "", + "name": "name", + "username": "", + "avatarURL": "avatar" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 604800 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 259200 + }, + "fileToken": { + "duration": 180 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + { + "id": "pbc_1430376151", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "Categories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2034676914", + "max": 0, + "min": 0, + "name": "cat_image_url", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2739402623", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2328411368", + "hidden": false, + "id": "relation3455582614", + "maxSelect": 1, + "minSelect": 0, + "name": "lesson_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_108570809", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "Customers", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3885137012", + "max": 0, + "min": 0, + "name": "email", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1146066909", + "max": 0, + "min": 0, + "name": "phone", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "number1813778413", + "max": null, + "min": null, + "name": "quota", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2063623452", + "max": 0, + "min": 0, + "name": "status", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file507207115", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "avatar_file", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation2809058197", + "maxSelect": 1, + "minSelect": 0, + "name": "user_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1196309394", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "LessonsCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1137421714", + "max": 0, + "min": 0, + "name": "cat_image_url", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2328411368", + "hidden": false, + "id": "relation3455582614", + "maxSelect": 1, + "minSelect": 0, + "name": "lesson_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "convertURLs": false, + "hidden": false, + "id": "editor1843675174", + "maxSize": 0, + "name": "description", + "presentable": false, + "required": false, + "system": false, + "type": "editor" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2328411368", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "LessonsTypes", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2363381545", + "max": 0, + "min": 0, + "name": "type", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "date1542800728", + "max": "", + "min": "", + "name": "field", + "presentable": false, + "required": false, + "system": false, + "type": "date" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_4061499106", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizCRCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3141885671", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizCRQuestions", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2416551515", + "max": 0, + "min": 0, + "name": "question_fh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2814132303", + "max": 0, + "min": 0, + "name": "question_sh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1249130051", + "max": 0, + "min": 0, + "name": "modal_ans", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_4061499106", + "hidden": false, + "id": "relation1827623476", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "json3493198471", + "maxSize": 0, + "name": "options", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3571292172", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2034676914", + "max": 0, + "min": 0, + "name": "cat_image", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_96745150", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizConnectives", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2416551515", + "max": 0, + "min": 0, + "name": "question_fh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2814132303", + "max": 0, + "min": 0, + "name": "question_sh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1249130051", + "max": 0, + "min": 0, + "name": "modal_ans", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_342761728", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 999, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_342761728", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizConnectivesCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3639453778", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizLPCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2560465762", + "max": 0, + "min": 0, + "name": "slug", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "convertURLs": false, + "hidden": false, + "id": "editor1843675174", + "maxSize": 0, + "name": "description", + "presentable": false, + "required": false, + "system": false, + "type": "editor" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_742947356", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizLPQuestions", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3639453778", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2560465762", + "max": 0, + "min": 0, + "name": "slug", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "convertURLs": false, + "hidden": false, + "id": "editor1843675174", + "maxSize": 0, + "name": "description", + "presentable": false, + "required": false, + "system": false, + "type": "editor" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2511066072", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizListenings", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3571292172", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 999, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_84667061", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizMFCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3346420851", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizMFQuestions", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3690399444", + "max": 0, + "min": 0, + "name": "word_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_84667061", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2936646783", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizMatchings", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3690399444", + "max": 0, + "min": 0, + "name": "word_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3571292172", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 999, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1305841361", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "UserMetas", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4192936109", + "max": 0, + "min": 0, + "name": "helloworld", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "json3622966325", + "maxSize": 0, + "name": "meta", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation2809058197", + "maxSelect": 1, + "minSelect": 0, + "name": "user_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2744374011", + "max": 0, + "min": 0, + "name": "state", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file376926767", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "avatar", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1466534506", + "max": 0, + "min": 0, + "name": "role", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1638686383", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "Vocabularies", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "file3309110367", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3690399444", + "max": 0, + "min": 0, + "name": "word_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text412313404", + "max": 0, + "min": 0, + "name": "sample_e", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4059087369", + "max": 0, + "min": 0, + "name": "sample_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1430376151", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text105650625", + "max": 0, + "min": 0, + "name": "category", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2328411368", + "hidden": false, + "id": "relation808508980", + "maxSelect": 1, + "minSelect": 0, + "name": "lesson_type_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_4275539003", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "name": "_authOrigins", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4228609354", + "max": 0, + "min": 0, + "name": "fingerprint", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_authOrigins_unique_pairs` ON `_authOrigins` (collectionRef, recordRef, fingerprint)" + ], + "system": true + }, + { + "id": "pbc_2281828961", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "name": "_externalAuths", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2462348188", + "max": 0, + "min": 0, + "name": "provider", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1044722854", + "max": 0, + "min": 0, + "name": "providerId", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_externalAuths_record_provider` ON `_externalAuths` (collectionRef, recordRef, provider)", + "CREATE UNIQUE INDEX `idx_externalAuths_collection_provider` ON `_externalAuths` (collectionRef, provider, providerId)" + ], + "system": true + }, + { + "id": "pbc_2279338944", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "_mfas", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1582905952", + "max": 0, + "min": 0, + "name": "method", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE INDEX `idx_mfas_collectionRef_recordRef` ON `_mfas` (collectionRef,recordRef)" + ], + "system": true + }, + { + "id": "pbc_1638494021", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "_otps", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 8, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 0, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text3866985172", + "max": 0, + "min": 0, + "name": "sentTo", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE INDEX `idx_otps_collectionRef_recordRef` ON `_otps` (collectionRef, recordRef)" + ], + "system": true + }, + { + "id": "pbc_1509025625", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "billingAddress", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1400097126", + "max": 0, + "min": 0, + "name": "country", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2744374011", + "max": 0, + "min": 0, + "name": "state", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text760939060", + "max": 0, + "min": 0, + "name": "city", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4114525948", + "max": 0, + "min": 0, + "name": "zipCode", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3620973610", + "max": 0, + "min": 0, + "name": "line1", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1322974608", + "max": 0, + "min": 0, + "name": "line2", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_123408445", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "helloworlds", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text907060870", + "max": 0, + "min": 0, + "name": "hello", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2109205374", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "t1", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text907060870", + "max": 0, + "min": 0, + "name": "hello", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "file2313559263", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "test_file", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + } + ], + "indexes": [], + "system": false + } +] +
+ +
+```` + +## File: schema.json +````json +[ + { + "id": "pbc_3142635823", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "_superusers", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_tokenKey_pbc_3142635823` ON `_superusers` (`tokenKey`)", + "CREATE UNIQUE INDEX `idx_email_pbc_3142635823` ON `_superusers` (`email`) WHERE `email` != ''" + ], + "system": true, + "authRule": "", + "manageRule": null, + "authAlert": { + "enabled": true, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "mappedFields": { + "id": "", + "name": "", + "username": "", + "avatarURL": "" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 86400 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 259200 + }, + "fileToken": { + "duration": 180 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + { + "id": "_pb_users_auth_", + "listRule": "id = @request.auth.id", + "viewRule": "id = @request.auth.id", + "createRule": "", + "updateRule": "id = @request.auth.id", + "deleteRule": "id = @request.auth.id", + "name": "users", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 255, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file376926767", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + "image/webp" + ], + "name": "avatar", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": null, + "type": "file" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_tokenKey__pb_users_auth_` ON `users` (`tokenKey`)", + "CREATE UNIQUE INDEX `idx_email__pb_users_auth_` ON `users` (`email`) WHERE `email` != ''" + ], + "system": false, + "authRule": "", + "manageRule": null, + "authAlert": { + "enabled": true, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "mappedFields": { + "id": "", + "name": "name", + "username": "", + "avatarURL": "avatar" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 604800 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 259200 + }, + "fileToken": { + "duration": 180 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + { + "id": "pbc_1430376151", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "Categories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2034676914", + "max": 0, + "min": 0, + "name": "cat_image_url", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2739402623", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2328411368", + "hidden": false, + "id": "relation3455582614", + "maxSelect": 1, + "minSelect": 0, + "name": "lesson_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_108570809", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "Customers", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3885137012", + "max": 0, + "min": 0, + "name": "email", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1146066909", + "max": 0, + "min": 0, + "name": "phone", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "number1813778413", + "max": null, + "min": null, + "name": "quota", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2063623452", + "max": 0, + "min": 0, + "name": "status", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file507207115", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "avatar_file", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation2809058197", + "maxSelect": 1, + "minSelect": 0, + "name": "user_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1196309394", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "LessonsCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1137421714", + "max": 0, + "min": 0, + "name": "cat_image_url", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2328411368", + "hidden": false, + "id": "relation3455582614", + "maxSelect": 1, + "minSelect": 0, + "name": "lesson_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "convertURLs": false, + "hidden": false, + "id": "editor1843675174", + "maxSize": 0, + "name": "description", + "presentable": false, + "required": false, + "system": false, + "type": "editor" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2328411368", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "LessonsTypes", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2363381545", + "max": 0, + "min": 0, + "name": "type", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "date1542800728", + "max": "", + "min": "", + "name": "field", + "presentable": false, + "required": false, + "system": false, + "type": "date" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_4061499106", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizCRCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3141885671", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizCRQuestions", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2416551515", + "max": 0, + "min": 0, + "name": "question_fh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2814132303", + "max": 0, + "min": 0, + "name": "question_sh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1249130051", + "max": 0, + "min": 0, + "name": "modal_ans", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_4061499106", + "hidden": false, + "id": "relation1827623476", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "json3493198471", + "maxSize": 0, + "name": "options", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3571292172", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2034676914", + "max": 0, + "min": 0, + "name": "cat_image", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_96745150", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizConnectives", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2416551515", + "max": 0, + "min": 0, + "name": "question_fh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2814132303", + "max": 0, + "min": 0, + "name": "question_sh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1249130051", + "max": 0, + "min": 0, + "name": "modal_ans", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_342761728", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 999, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_342761728", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizConnectivesCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3639453778", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizLPCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2560465762", + "max": 0, + "min": 0, + "name": "slug", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "convertURLs": false, + "hidden": false, + "id": "editor1843675174", + "maxSize": 0, + "name": "description", + "presentable": false, + "required": false, + "system": false, + "type": "editor" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_742947356", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizLPQuestions", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3639453778", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2560465762", + "max": 0, + "min": 0, + "name": "slug", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "convertURLs": false, + "hidden": false, + "id": "editor1843675174", + "maxSize": 0, + "name": "description", + "presentable": false, + "required": false, + "system": false, + "type": "editor" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2511066072", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizListenings", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3571292172", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 999, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_84667061", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizMFCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3346420851", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizMFQuestions", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3690399444", + "max": 0, + "min": 0, + "name": "word_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_84667061", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2936646783", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizMatchings", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3690399444", + "max": 0, + "min": 0, + "name": "word_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3571292172", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 999, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1305841361", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "UserMetas", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4192936109", + "max": 0, + "min": 0, + "name": "helloworld", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "json3622966325", + "maxSize": 0, + "name": "meta", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation2809058197", + "maxSelect": 1, + "minSelect": 0, + "name": "user_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2744374011", + "max": 0, + "min": 0, + "name": "state", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file376926767", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "avatar", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1466534506", + "max": 0, + "min": 0, + "name": "role", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1638686383", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "Vocabularies", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "file3309110367", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3690399444", + "max": 0, + "min": 0, + "name": "word_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text412313404", + "max": 0, + "min": 0, + "name": "sample_e", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4059087369", + "max": 0, + "min": 0, + "name": "sample_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1430376151", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text105650625", + "max": 0, + "min": 0, + "name": "category", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2328411368", + "hidden": false, + "id": "relation808508980", + "maxSelect": 1, + "minSelect": 0, + "name": "lesson_type_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_4275539003", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "name": "_authOrigins", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4228609354", + "max": 0, + "min": 0, + "name": "fingerprint", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_authOrigins_unique_pairs` ON `_authOrigins` (collectionRef, recordRef, fingerprint)" + ], + "system": true + }, + { + "id": "pbc_2281828961", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "name": "_externalAuths", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2462348188", + "max": 0, + "min": 0, + "name": "provider", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1044722854", + "max": 0, + "min": 0, + "name": "providerId", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_externalAuths_record_provider` ON `_externalAuths` (collectionRef, recordRef, provider)", + "CREATE UNIQUE INDEX `idx_externalAuths_collection_provider` ON `_externalAuths` (collectionRef, provider, providerId)" + ], + "system": true + }, + { + "id": "pbc_2279338944", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "_mfas", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1582905952", + "max": 0, + "min": 0, + "name": "method", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE INDEX `idx_mfas_collectionRef_recordRef` ON `_mfas` (collectionRef,recordRef)" + ], + "system": true + }, + { + "id": "pbc_1638494021", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "_otps", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 8, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 0, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text3866985172", + "max": 0, + "min": 0, + "name": "sentTo", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE INDEX `idx_otps_collectionRef_recordRef` ON `_otps` (collectionRef, recordRef)" + ], + "system": true + }, + { + "id": "pbc_1509025625", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "billingAddress", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1400097126", + "max": 0, + "min": 0, + "name": "country", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2744374011", + "max": 0, + "min": 0, + "name": "state", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text760939060", + "max": 0, + "min": 0, + "name": "city", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4114525948", + "max": 0, + "min": 0, + "name": "zipCode", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3620973610", + "max": 0, + "min": 0, + "name": "line1", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1322974608", + "max": 0, + "min": 0, + "name": "line2", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_123408445", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "helloworlds", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text907060870", + "max": 0, + "min": 0, + "name": "hello", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2109205374", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "t1", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text907060870", + "max": 0, + "min": 0, + "name": "hello", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "file2313559263", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "test_file", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + } + ], + "indexes": [], + "system": false + } +] +```` diff --git a/002_source/cms/src/db/repomix-output.xml b/002_source/cms/src/db/repomix-output.xml new file mode 100644 index 0000000..12e51d0 --- /dev/null +++ b/002_source/cms/src/db/repomix-output.xml @@ -0,0 +1,6168 @@ +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) + + + + + + + + + +_PROMPT/ + 1.MD + 2.MD + 3.MD + 4.md + temp.md +Customers/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetActiveCount.tsx + GetAll.tsx + GetAllCount.tsx + GetBlockedCount.tsx + GetById.tsx + GetPendingCount.tsx + Helloworld.tsx + Update.tsx +Events/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Update.tsx.draft +Helloworlds/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Helloworld.tsx.draft + Update.tsx.draft +LessonCategories/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Helloworld.tsx + Update.tsx +LessonTypes/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Helloworld.tsx + Update.tsx +Messages/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Update.tsx.draft +QuizCategories/ + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Helloworld.tsx.draft + Update.tsx.draft +QuizConnectives/ + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Helloworld.tsx.draft + Update.tsx.draft +QuizConnectivesCategories/ + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Helloworld.tsx.draft + Update.tsx.draft +QuizCRCategories/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Update.tsx +QuizCRQuestions/ + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx +QuizListenings/ + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + ListWithOption.tsx +QuizLPCategories/ + _GUIDELINES.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Update.tsx +QuizLPQuestions/ + _GUIDELINES.md + _PROMPT.md + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Update.tsx +QuizMFCategories/ + Create.tsx + Delete.tsx + GetAll.tsx + GetAllCount.tsx + GetById.tsx + GetHiddenCount.tsx + GetVisibleCount.tsx + Update.tsx +Students/ + Create.tsx + Delete.tsx + GetActiveCount.tsx + GetAll.tsx + GetAllCount.tsx + GetBlockedCount.tsx + GetById.tsx + Update.tsx +Subscriptions/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx.draft + GetById.tsx.draft + GetHiddenCount.tsx.draft + GetVisibleCount.tsx.draft + Update.tsx.draft +Teachers/ + _GUIDELINES.md + Create.tsx + GetActiveCount.tsx + GetAll.tsx + GetAllCount.tsx + GetBlockedCount.tsx + GetById.tsx + GetPendingCount.tsx + Update.tsx +UserMetas/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx + GetById.tsx.draft + Update.tsx.draft +Users/ + _GUIDELINES.md + Create.tsx.draft + Delete.tsx.draft + GetAll.tsx.draft + GetAllCount.tsx + GetById.tsx.draft + Update.tsx.draft +DB_AI_GUIDELINE.MD +schema.json + + + +This section contains the contents of the repository's files. + + +Hi, please study the documentation below, +i will send you the task afterwards, + +please read and understand the documentation below and link up the ideas +reply `OK` when you done +no need to state me any other things, thanks + +1. `schema.dbml` + +- this describe the database schema in dbml format +- filepath: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml` + +2. `schema.json` + +- this is the schema export in pocketbase format +- filepath: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/schema.json` + +3. `_AI_GUIDELINE`: + +- there are the markdown files that help you better understand the implementation +- directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/_AI_GUIDELINE` + +thanks + +--- + +# task + +clone from `LessonTypes` to `Customers` + +## steps + +1. read `tsx` files from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/LessonTypes` +1. copy file to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers` +1. modify the copied `tsx` files to suit `customer` fields + + + +update `LpCategoryDefaultValue` +in file `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.ts` + +thanks + +you can find the type def in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` + +please help to draft code file: + +base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db` + +using +`$base_dir/QuizListenings/GetHiddenCount.tsx`, +`$base_dir/QuizListenings/GetVisibleCount.tsx`, +`$base_dir/LessonTypes/GetHiddenCount.tsx`, +`$base_dir/LessonTypes/GetVisibleCount.tsx`, +as reference, + +look into the all directories under base_dir e.g. `QuizCategories`. +propergate `GetHiddenCount.tsx` and `GetVisibleCount.tsx` if missing, do the change to suit the collection. +use `.draft.tsx` instead when you write file + +--- + +rewrite `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/LessonCategories/GetAllCount.tsx` to match `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/UserMetas/GetAllCount.tsx` style + +--- + +style rewrite + +study +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/overview/summary/ActiveUserCount/index.tsx` +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/overview/summary/LessonCategoriesCount/index.tsx` + +and rewrite `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/overview/summary/LessonTypeCount/index.tsx` to match style above thanks + + + +please draft with idea: + +``` +await pb +.collection(COL_LESSON_TYPES) +.getList(currentPage + 1, rowsPerPage, listOption); +``` + +for Listening Practice + +thanks + +I want you to clone +from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/LessonTypes/GetVisibleCount.tsx` (source file) + +to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizListenings/GetVisibleCount.tsx` (dest file) + +please extract , link up and remember the document properties +(e.g. types, functions, variables, constants, etc) +from source file +draft dest file + +update the variables and properties of dest file to reflect `listening practice categories`/`lp_categories` + +--- + +## task + +update `schema.dbml` to reflect `schema.json` + +## details + +Hi, +I have a pocketbase export json file: +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/pocketbase/pb_hooks/seed/schema.json` + +and a dbml file: +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml` + +the collection name in pocketbase should be reflected by a table in dbml, + +## steps + +compare `schema.json` and `schema.dbml` +please keep `schema.json` remain unchanged +update `schema.dbml` to reflect `schema.json` +do check again when finished + + + +--- + +clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it + +please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden` + +well done !, please proceed to another request + +working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db` + +according information from `schema.json`, get the collection of `Students` + +pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content + +when you draft coding, review file and append with `.tsx.draft` + +--- + +- this is part of react typescript project, with pocketbase +- `schema.dbml`, describe the collections(tables) +- folder `LessonCategories`, the correct references +- folder `LessonTypes`, the correct references +- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006` +- do not read root directory, assume it is a fresh copy of nextjs project is ok + +## instruction + +- break the questions into smaller parts +- review file append with `.draft`, see if the content aligned with the correct references +- read and understand `dbml` file +- lookup the every folder + +## tasks + +Thanks + +--- + +please take a look in `schema.dbml` and `schema.json`, +associate the collection from json file to the table in dbml file + +please modify the `schema.dbml` to align with `schema.json` + +to the collection `QuizLPCategories` align the dbml file in the previous prompt + +--- + +please revise + +please revise +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory` + +to the collection `QuizLPCategories` align the dbml file in the previous prompt + +please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx` + +to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory` + + + +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/lp-categories-filters.tsx` + +this file is original for `lesson_category` model, +please modify it to fit `lp_category` (listening practice category) + +thanks + + + +# GUIDELINES + +This folder contains drivers for `Customer`/`Customers` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx, GetActiveCount.tsx, GetBlockedCount.tsx, GetPendingCount.tsx) +- misc (Helloworld.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; + +export async function createCustomer(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` + + + +// api method for crate customer record +// RULES: +// TBA +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; +import type { CreateFormProps } from '@/components/dashboard/customer/type.d'; +import type { RecordModel } from 'pocketbase'; + +export async function createCustomer(data: CreateFormProps): Promise { + return pb.collection(COL_CUSTOMERS).create(data); +} + + + +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; + +export async function deleteCustomer(id: string): Promise { + return pb.collection(COL_CUSTOMERS).delete(id); +} + + + +import { COL_CUSTOMERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetActiveCount(): Promise { + const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, { + filter: 'status = "active"', + }); + return count; +} + + + +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getAllCustomers(options = {}): Promise { + return pb.collection(COL_CUSTOMERS).getFullList(options); +} + + + +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; + +export async function getAllCustomersCount(): Promise { + const result = await pb.collection(COL_CUSTOMERS).getList(1, 1); + return result.totalItems; +} + + + +import { COL_CUSTOMERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetBlockedCount(): Promise { + const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, { + filter: 'status = "blocked"', + }); + return count; +} + + + +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getCustomerById(id: string): Promise { + return pb.collection(COL_CUSTOMERS).getOne(id); +} + + + +import { COL_CUSTOMERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetPendingCount(): Promise { + const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, { + filter: 'status = "pending"', + }); + return count; +} + + + +export function helloCustomer() { + return 'Hello from Customers module!'; +} + + + +import { pb } from '@/lib/pb'; +import { COL_CUSTOMERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import type { EditFormProps } from '@/components/dashboard/customer/type.d'; + +export async function updateCustomer(id: string, data: Partial): Promise { + return pb.collection(COL_CUSTOMERS).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `Event`/`Events` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Events/type.d.tsx` +- Event records require special handling for: + - Date/time validation + - Location data + - Attendee management + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createEvent(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Events')) +} +``` + + + +import { COL_EVENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/event/types'; + +export default function createEvent(data: CreateForm): Promise { + return pb.collection(COL_EVENTS).create(data); +} + + + +import { COL_EVENTS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteEvent(id: string): Promise { + return pb.collection(COL_EVENTS).delete(id); +} + + + +import { COL_EVENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllEvents(): Promise { + return pb.collection(COL_EVENTS).getFullList({ + sort: 'event_time' // Sort by event time + }); +} + + + +import { COL_EVENTS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getAllEventsCount(startDate?: string, endDate?: string): Promise { + let filter = ''; + if (startDate && endDate) { + filter = `event_time >= "${startDate}" && event_time <= "${endDate}"`; + } else if (startDate) { + filter = `event_time >= "${startDate}"`; + } else if (endDate) { + filter = `event_time <= "${endDate}"`; + } + + const { totalItems: count } = await pb + .collection(COL_EVENTS) + .getList(1, 9999, { filter }); + return count; +} + + + +import { COL_EVENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getEventById(id: string): Promise { + return pb.collection(COL_EVENTS).getOne(id); +} + + + +// REQ0006 +import { COL_EVENTS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_EVENTS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_EVENTS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_EVENTS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_EVENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { UpdateForm } from '@/components/dashboard/event/types'; + +export default function updateEvent(id: string, data: UpdateForm): Promise { + return pb.collection(COL_EVENTS).update(id, data); +} + + + +# GUIDELINES + +This folder contains test drivers for `Helloworld` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function +- type information defined in `@/db/Helloworlds/type.d.tsx` +- This is a test collection - keep implementations simple + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createHelloworld(data: CreateFormProps) { + // Simple test implementation + return pb.collection('Helloworlds').create(data); +} +``` + + + +import { COL_HELLOWORLDS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/helloworld/types'; + +export default function createHelloworld(data: CreateForm): Promise { + return pb.collection(COL_HELLOWORLDS).create(data); +} + + + +import { COL_HELLOWORLDS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default function deleteHelloworld(id: string): Promise { + return pb.collection(COL_HELLOWORLDS).delete(id); +} + + + +import { COL_HELLOWORLDS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllHelloworlds(): Promise { + return pb.collection(COL_HELLOWORLDS).getFullList(); +} + + + +import { COL_HELLOWORLDS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getHelloworldById(id: string): Promise { + return pb.collection(COL_HELLOWORLDS).getOne(id); +} + + + +// REQ0006 +import { COL_HELLOWORLDS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_HELLOWORLDS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_HELLOWORLDS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_HELLOWORLDS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; + + + +import { COL_HELLOWORLDS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/helloworld/types'; + +export default function updateHelloworld(id: string, data: CreateForm): Promise { + return pb.collection(COL_HELLOWORLDS).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `LessonCategory`/`LessonCategories` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/LessonCategories/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_LESSON_CATEGORIES } from '@/constants'; + +export async function createLessonCategory(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` + + + +import { COL_LESSON_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function createLessonCategory(data: CreateFormProps): Promise { + return pb.collection(COL_LESSON_CATEGORIES).create(data); +} + + + +import { COL_LESSON_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteLessonCategory(id: string): Promise { + return pb.collection(COL_LESSON_CATEGORIES).delete(id); +} + + + +import { COL_LESSON_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllLessonCategories(): Promise { + return pb.collection(COL_LESSON_CATEGORIES).getFullList(); +} + + + +// RULES: +// error handled by caller +// contain definition to collection only + +import { COL_LESSON_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function getAllLessonCategoriesCount(): Promise { + return pb + .collection(COL_LESSON_CATEGORIES) + .getList(1, 9999) + .then((res) => res.totalItems); +} + + + +import { COL_LESSON_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getLessonCategoryById(id: string): Promise { + return pb.collection(COL_LESSON_CATEGORIES).getOne(id); +} + + + +// REQ0006 +import { COL_LESSON_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_LESSON_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; + + + +import { COL_LESSON_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function updateLessonCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_LESSON_CATEGORIES).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `LessonType`/`LessonTypes` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/LessonTypes/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_LESSON_TYPES } from '@/constants'; + +export async function createLessonType(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` + + + +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/lesson_type/lesson-type'; + +export default function createLessonType(data: CreateForm): Promise { + return pb.collection(COL_LESSON_TYPES).create(data); +} + + + +import { COL_LESSON_TYPES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteLessonType(id: string): Promise { + return pb.collection(COL_LESSON_TYPES).delete(id); +} + + + +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllLessonTypes(): Promise { + return pb.collection(COL_LESSON_TYPES).getFullList(); +} + + + +// REQ0006 +import { COL_LESSON_TYPES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_TYPES).getList(1, 9999, {}); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getLessonTypeById(id: string): Promise { + return pb.collection(COL_LESSON_TYPES).getOne(id); +} + + + +// REQ0006 +import { COL_LESSON_TYPES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_TYPES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_LESSON_TYPES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_LESSON_TYPES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; + + + +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/lesson_type/lesson-type'; + +export default function updateLessonType(id: string, data: CreateForm): Promise { + return pb.collection(COL_LESSON_TYPES).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `Message`/`Messages` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Messages/type.d.tsx` +- Message records require special handling for: + - Sender/receiver validation + - Timestamp management + - Read status tracking + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createMessage(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Messages')) +} +``` + + + +import { COL_MESSAGES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/message/types'; + +export default function createMessage(data: CreateForm): Promise { + return pb.collection(COL_MESSAGES).create(data); +} + + + +import { COL_MESSAGES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteMessage(id: string): Promise { + return pb.collection(COL_MESSAGES).delete(id); +} + + + +import { COL_MESSAGES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllMessages(): Promise { + return pb.collection(COL_MESSAGES).getFullList({ + expand: 'user_id', // Expand related user data + sort: '-created' // Sort by most recent first + }); +} + + + +import { COL_MESSAGES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getAllMessagesCount(status?: string): Promise { + const filter = status ? `status = "${status}"` : ''; + const { totalItems: count } = await pb + .collection(COL_MESSAGES) + .getList(1, 9999, { filter }); + return count; +} + + + +import { COL_MESSAGES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getMessageById(id: string): Promise { + return pb.collection(COL_MESSAGES).getOne(id, { + expand: 'user_id' // Expand related user data + }); +} + + + +// REQ0006 +import { COL_MESSAGES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_MESSAGES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_MESSAGES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_MESSAGES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_MESSAGES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { UpdateForm } from '@/components/dashboard/message/types'; + +export default function updateMessage(id: string, data: UpdateForm): Promise { + return pb.collection(COL_MESSAGES).update(id, data); +} + + + +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface CreateForm { + // TODO: Add QuizCategories fields +} + +export default function createQuizCategory(data: CreateForm): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).create(data); +} + + + +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizCategory(id: string): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).delete(id); +} + + + +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizCategories(): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).getFullList(); +} + + + +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; + + + +import { COL_QUIZ_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface UpdateForm { + // TODO: Add QuizCategories fields +} + +export default function updateQuizCategory(id: string, data: UpdateForm): Promise { + return pb.collection(COL_QUIZ_CATEGORIES).update(id, data); +} + + + +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface CreateForm { + // TODO: Add QuizConnectives fields +} + +export default function createQuizConnective(data: CreateForm): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).create(data); +} + + + +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizConnective(id: string): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).delete(id); +} + + + +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizConnectives(): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).getFullList(); +} + + + +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizConnectiveById(id: string): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_CONNECTIVES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CONNECTIVES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_CONNECTIVES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CONNECTIVES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; + + + +import { COL_QUIZ_CONNECTIVES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface UpdateForm { + // TODO: Add QuizConnectives fields +} + +export default function updateQuizConnective(id: string, data: UpdateForm): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES).update(id, data); +} + + + +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface CreateForm { + // TODO: Add QuizConnectivesCategories fields +} + +export default function createQuizConnectivesCategory(data: CreateForm): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).create(data); +} + + + +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizConnectivesCategory(id: string): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).delete(id); +} + + + +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizConnectivesCategories(): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).getFullList(); +} + + + +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizConnectivesCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb + .collection(COL_QUIZ_CONNECTIVES_CATEGORIES) + .getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb + .collection(COL_QUIZ_CONNECTIVES_CATEGORIES) + .getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; + + + +import { COL_QUIZ_CONNECTIVES_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface UpdateForm { + // TODO: Add QuizConnectivesCategories fields +} + +export default function updateQuizConnectivesCategory(id: string, data: UpdateForm): Promise { + return pb.collection(COL_QUIZ_CONNECTIVES_CATEGORIES).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `QuizCRCategory`/`QuizCRCategories` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/QuizCRCategories/type.d.tsx` +- Quiz categories may require additional validation logic + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; + +export async function createQuizCRCategory(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` + + + +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import { CreateFormProps } from '@/components/dashboard/cr/categories/type'; + +export default function createQuizCRCategory(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).create(data); +} + + + +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizCRCategories(id: string): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).delete(id); +} + + + +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import { pb } from '@/lib/pb'; + +export default function getAllQuizCRCategories(): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).getFullList(); +} + + + +// REQ0006 +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_CR_CATEGORIES).getList(1, 9999, {}); + return count; +} + + + +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizCRCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizCRCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CR_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizCRCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CR_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_QUIZ_CR_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/cr/categories/type'; + +export default function updateQuizCRCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_CR_CATEGORIES).update(id, data); +} + + + +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/cr/questions/type'; + +// interface CreateForm { +// // TODO: Add QuizCRQuestions fields +// } + +export default function createQuizCRQuestion(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_CR_QUESTIONS).create(data); +} + + + +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizCRQuestions(id: string): Promise { + return pb.collection(COL_QUIZ_CR_QUESTIONS).delete(id); +} + + + +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizCRQuestions(): Promise { + return pb.collection(COL_QUIZ_CR_QUESTIONS).getFullList(); +} + + + +// REQ0006 +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_CR_QUESTIONS).getList(1, 9999, {}); + return count; +} + + + +// REQ0006 +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizCRQuestionsCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CR_QUESTIONS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_CR_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizCRQuestionsCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_CR_QUESTIONS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizLPCategories(id: string): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).delete(id); +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizListenings(): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getFullList(); +} + + + +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, {}); + return count; +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizListeningById(id: string): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { ListResult, RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +interface ListWithOptionParams { + currentPage: number; + rowsPerPage: number; + listOption?: { + filter?: string; + sort?: string; + expand?: string; + }; +} + +export default function listWithOption({ + currentPage, + rowsPerPage, + listOption = {}, +}: ListWithOptionParams): Promise> { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getList(currentPage + 1, rowsPerPage, listOption); +} + + + +# GUIDELINES + +This folder contains drivers for `QuizLPCategory` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/QuizLPCategories/type.d.tsx` +- Quiz LP categories may require additional validation logic + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +export async function createQuizLPCategory(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(COL_QUIZ_LP_CATEGORIES)) +} +``` + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function createQuizLPCategory(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).create(data); +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizLPCategories(id: string): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).delete(id); +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizLPCategories(): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getFullList(); +} + + + +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, {}); + return count; +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizLPCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizLPCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizLPCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function updateQuizLPCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `QuizLPQuestion` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/QuizLPQuestions/type.d.tsx` +- Quiz LP questions require special handling for: + - Answer validation + - Question type checking + - Category association + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +export async function createQuizLPQuestion(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(COL_QUIZ_LP_QUESTIONS)) +} +``` + + + +please help to review the `tsx` file in this folder +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPQuestions` + +it was clone from +`LPCategories` +please help to modify to +`LPQuestions` + +please also help to modify the name of +`variables`, `constants`, `functions`, `classes`, components's name, paths + +the db fields structures between them are the same + +do not move the files +do not create directories +keep current folder structure is important + +thanks + + + +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/questions/type'; + +// interface CreateForm { +// // TODO: Add QuizLPQuestions fields +// } + +export default function createQuizLPQuestion(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).create(data); +} + + + +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizLPQuestions(id: string): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).delete(id); +} + + + +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizLPQuestions(): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).getFullList(); +} + + + +// REQ0006 +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_LP_QUESTIONS).getList(1, 9999, {}); + return count; +} + + + +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizLPQuestionById(id: string): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizLPQuestionsCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizLPQuestionsCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_QUIZ_LP_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/lp/categories/type'; + +export default function updateQuizLPCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_CATEGORIES).update(id, data); +} + + + +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/mf/categories/type'; + +export default function createQuizMFCategory(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).create(data); +} + + + +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteQuizMFCategories(id: string): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).delete(id); +} + + + +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllQuizMFCategories(): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).getFullList(); +} + + + +// REQ0006 +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + const { totalItems: count } = await pb.collection(COL_QUIZ_MF_CATEGORIES).getList(1, 9999, {}); + return count; +} + + + +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getQuizMFCategoryById(id: string): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).getOne(id); +} + + + +// REQ0006 +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getHiddenQuizMFCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getVisibleQuizMFCategoriesCount(): Promise { + try { + const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/mf/categories/type'; + +export default function updateQuizMFCategory(id: string, data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_MF_CATEGORIES).update(id, data); +} + + + +// api method for create student record +// RULES: +// TBA +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; +import type { CreateFormProps } from '@/components/dashboard/student/type.d'; +import type { RecordModel } from 'pocketbase'; + +export async function createStudent(data: CreateFormProps): Promise { + return pb.collection(COL_STUDENTS).create(data); +} + + + +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; + +export async function deleteStudent(id: string): Promise { + return pb.collection(COL_STUDENTS).delete(id); +} + + + +import { COL_STUDENTS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetActiveCount(): Promise { + const { totalItems: count } = await pb.collection(COL_STUDENTS).getList(1, 1, { + filter: 'status = "active"', + }); + return count; +} + + + +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getAllStudents(options = {}): Promise { + return pb.collection(COL_STUDENTS).getFullList(options); +} + + + +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; + +export async function getAllStudentsCount(): Promise { + const result = await pb.collection(COL_STUDENTS).getList(1, 1); + return result.totalItems; +} + + + +import { COL_STUDENTS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetBlockedCount(): Promise { + const { totalItems: count } = await pb.collection(COL_STUDENTS).getList(1, 1, { + filter: 'status = "blocked"', + }); + return count; +} + + + +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getStudentById(id: string): Promise { + return pb.collection(COL_STUDENTS).getOne(id); +} + + + +import { pb } from '@/lib/pb'; +import { COL_STUDENTS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import type { EditFormProps } from '@/components/dashboard/student/type.d'; + +export async function updateStudent(id: string, data: Partial): Promise { + return pb.collection(COL_STUDENTS).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `Subscription`/`Subscriptions` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Subscriptions/type.d.tsx` +- Subscription records require special handling for: + - Payment status validation + - Expiration date checks + - Auto-renewal logic + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createSubscription(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Subscriptions')) +} +``` + + + +import { COL_SUBSCRIPTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/subscription/types'; + +export default function createSubscription(data: CreateForm): Promise { + return pb.collection(COL_SUBSCRIPTIONS).create(data); +} + + + +import { COL_SUBSCRIPTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteSubscription(id: string): Promise { + // TODO: Add validation for active subscriptions if needed + return pb.collection(COL_SUBSCRIPTIONS).delete(id); +} + + + +import { COL_SUBSCRIPTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllSubscriptions(): Promise { + return pb.collection(COL_SUBSCRIPTIONS).getFullList({ + expand: 'user_id,plan_id', // Expand related user and plan data + sort: '-created' // Sort by most recent first + }); +} + + + +import { COL_SUBSCRIPTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function getAllSubscriptionsCount(status?: string): Promise { + const filter = status ? `status = "${status}"` : ''; + const { totalItems: count } = await pb + .collection(COL_SUBSCRIPTIONS) + .getList(1, 9999, { filter }); + return count; +} + + + +import { COL_SUBSCRIPTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getSubscriptionById(id: string): Promise { + return pb.collection(COL_SUBSCRIPTIONS).getOne(id, { + expand: 'user_id,plan_id' // Expand related user and plan data + }); +} + + + +// REQ0006 +import { COL_SUBSCRIPTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetHiddenCount(): Promise { + try { + const result = await pb.collection(COL_SUBSCRIPTIONS).getList(1, 9999, { filter: 'visible = "hidden"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +// REQ0006 +import { COL_SUBSCRIPTIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetVisibleCount(): Promise { + try { + const result = await pb.collection(COL_SUBSCRIPTIONS).getList(1, 9999, { filter: 'visible = "visible"' }); + const { totalItems: count } = result; + return count; + } catch (error) { + return 0; + } +} + + + +import { COL_SUBSCRIPTIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { UpdateForm } from '@/components/dashboard/subscription/types'; + +export default function updateSubscription(id: string, data: UpdateForm): Promise { + return pb.collection(COL_SUBSCRIPTIONS).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `Teacher`/`Teachers` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Teachers/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; + +export async function createTeacher(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection('Teachers')) +} +``` + + + +// api method for create teacher record +// RULES: +// TBA +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; +import type { CreateFormProps } from '@/components/dashboard/teacher/type.d'; +import type { RecordModel } from 'pocketbase'; + +export async function createTeacher(data: CreateFormProps): Promise { + return pb.collection(COL_TEACHERS).create(data); +} + + + +import { COL_TEACHERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetActiveCount(): Promise { + const { totalItems: count } = await pb.collection(COL_TEACHERS).getList(1, 1, { + filter: 'status = "active"', + }); + return count; +} + + + +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getAllTeachers(options = {}): Promise { + return pb.collection(COL_TEACHERS).getFullList(options); +} + + + +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; + +export async function getAllTeachersCount(): Promise { + const result = await pb.collection(COL_TEACHERS).getList(1, 1); + return result.totalItems; +} + + + +import { COL_TEACHERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetBlockedCount(): Promise { + const { totalItems: count } = await pb.collection(COL_TEACHERS).getList(1, 1, { + filter: 'status = "blocked"', + }); + return count; +} + + + +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; +import { RecordModel } from 'pocketbase'; + +export async function getTeacherById(id: string): Promise { + return pb.collection(COL_TEACHERS).getOne(id); +} + + + +import { COL_TEACHERS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default async function GetPendingCount(): Promise { + const { totalItems: count } = await pb.collection(COL_TEACHERS).getList(1, 1, { + filter: 'status = "pending"', + }); + return count; +} + + + +import { pb } from '@/lib/pb'; +import { COL_TEACHERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import type { EditFormProps } from '@/components/dashboard/teacher/type.d'; + +export async function updateTeacher(id: string, data: Partial): Promise { + return pb.collection(COL_TEACHERS).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `UserMeta`/`UserMetas` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/UserMetas/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_USER_METAS } from '@/constants'; + +export async function createUserMeta(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` + + + +import { COL_LESSON_TYPES } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/lesson_type/types'; + +// import type { CreateForm } from '@/components/dashboard/lesson_type/interfaces.ts.del'; + +export default function createLessonType(data: CreateForm): Promise { + return pb.collection(COL_LESSON_TYPES).create(data); +} + + + +import { COL_USER_METAS } from '@/constants'; +import { pb } from '@/lib/pb'; + +export default function deleteUserMeta(id: string): Promise { + return pb.collection(COL_USER_METAS).delete(id); +} + + + +import { COL_USER_METAS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import { pb } from '@/lib/pb'; + +export default function getAllUserMetas(): Promise { + return pb.collection(COL_USER_METAS).getFullList(); +} + + + +// RULES: +// error handled by caller +// contain definition to collection only + +import { COL_USER_METAS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function getAllUserMetasCount(): Promise { + return pb + .collection(COL_USER_METAS) + .getList(1, 9998) + .then((res) => res.totalItems); +} + + + +import { COL_USER_METAS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import { pb } from '@/lib/pb'; + +export default function getUserMetaById(id: string): Promise { + return pb.collection(COL_USER_METAS).getOne(id); +} + + + +import { COL_USER_METAS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/user_meta/types'; + +export default function updateUserMeta(id: string, data: CreateForm): Promise { + return pb.collection(COL_USER_METAS).update(id, data); +} + + + +# GUIDELINES + +This folder contains drivers for `User`/`Users` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Users/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_USERS } from '@/constants'; + +export async function createUser(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` + + + +import { COL_USERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/user/types'; + + +export default function createUser(data: CreateForm): Promise { + return pb.collection(COL_USERS).create(data); +} + + + +import { COL_USERS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default function deleteUser(id: string): Promise { + return pb.collection(COL_USERS).delete(id); +} + + + +import { COL_USERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getAllUsers(): Promise { + return pb.collection(COL_USERS).getFullList(); +} + + + +// REQ0006 +import { COL_USERS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + try { + const result = await pb.collection(`users`).getList(1, 9999, { filter: 'email != ""' }); + const { totalItems: count } = result; + return count; + } catch (error) { + console.error(error); + return -99; + } +} + + + +import { COL_USERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; + +export default function getUserById(id: string): Promise { + return pb.collection(COL_USERS).getOne(id); +} + + + +import { COL_USERS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; + +import { pb } from '@/lib/pb'; +import type { CreateForm } from '@/components/dashboard/user/types'; + +export default function updateUser(id: string, data: CreateForm): Promise { + return pb.collection(COL_USERS).update(id, data); +} + + + +# AI GUIDELINE + +## getting started + +Imagine there is a software developer and a QA engineer to solve the problems together + +They will: + +no need to reply me what you are going on and your digest in this phase. +just reply me "OK" when done + +base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project` + +- read `/001_documentation/Requirements/REQ0006/schema.dbml` +this is file in dbml syntax state the main database + +- read `/002_source/cms/src/db/schema.json` +this is the file of live pocketbase schema output + +- read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts` +this is the content of `@/constants` + +- look into the md files in folder `/002_source/cms/_AI_GUIDELINE` + +- read, remember and link up the ideas in file stated above, +i will tell them the task afterwards + +--- + +The software engineer will provide solutions, +while QA engineer will feedback the opinion. + +this is now not in debug phase, +so, no need to reply me what they are going on or their insight throught the prompt. +just reply me "OK" when done + +--- + +clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it + +please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden` + +well done !, please proceed to another request + +working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db` + +according information from `schema.json`, get the collection of `Students` + +pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content + +when you draft coding, review file and append with `.tsx.draft` + +--- + +- this is part of react typescript project, with pocketbase +- `schema.dbml`, describe the collections(tables) +- folder `LessonCategories`, the correct references +- folder `LessonTypes`, the correct references +- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006` +- do not read root directory, assume it is a fresh copy of nextjs project is ok + +## instruction + +- break the questions into smaller parts +- review file append with `.draft`, see if the content aligned with the correct references +- read and understand `dbml` file +- lookup the every folder + +## tasks + +Thanks + + + +--- + + +please take a look in `schema.dbml` and `schema.json`, +associate the collection from json file to the table in dbml file + +please modify the `schema.dbml` to align with `schema.json` + +to the collection `QuizLPCategories` align the dbml file in the previous prompt + + +--- + +please revise + +please revise +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory` + +to the collection `QuizLPCategories` align the dbml file in the previous prompt + + +please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx` + +to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory` + +--- + +the constants file (`@/constants`) was `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts` + +please help to fix the `tsx` files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizMFCategories`, +the `COL` constants is wrongly used, it should refer to `COL_QUIZ_MF_CATEGORIES`. thanks + + +please update the `COL_XXXX` TO COL_MF_CATEGORIES + + + +[ + { + "id": "pbc_3142635823", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "_superusers", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_tokenKey_pbc_3142635823` ON `_superusers` (`tokenKey`)", + "CREATE UNIQUE INDEX `idx_email_pbc_3142635823` ON `_superusers` (`email`) WHERE `email` != ''" + ], + "system": true, + "authRule": "", + "manageRule": null, + "authAlert": { + "enabled": true, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "mappedFields": { + "id": "", + "name": "", + "username": "", + "avatarURL": "" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 86400 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 259200 + }, + "fileToken": { + "duration": 180 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + { + "id": "_pb_users_auth_", + "listRule": "id = @request.auth.id", + "viewRule": "id = @request.auth.id", + "createRule": "", + "updateRule": "id = @request.auth.id", + "deleteRule": "id = @request.auth.id", + "name": "users", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 255, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file376926767", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + "image/webp" + ], + "name": "avatar", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": null, + "type": "file" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_tokenKey__pb_users_auth_` ON `users` (`tokenKey`)", + "CREATE UNIQUE INDEX `idx_email__pb_users_auth_` ON `users` (`email`) WHERE `email` != ''" + ], + "system": false, + "authRule": "", + "manageRule": null, + "authAlert": { + "enabled": true, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "mappedFields": { + "id": "", + "name": "name", + "username": "", + "avatarURL": "avatar" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 604800 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 259200 + }, + "fileToken": { + "duration": 180 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + { + "id": "pbc_1430376151", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "Categories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2034676914", + "max": 0, + "min": 0, + "name": "cat_image_url", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2739402623", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2328411368", + "hidden": false, + "id": "relation3455582614", + "maxSelect": 1, + "minSelect": 0, + "name": "lesson_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_108570809", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "Customers", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3885137012", + "max": 0, + "min": 0, + "name": "email", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1146066909", + "max": 0, + "min": 0, + "name": "phone", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "number1813778413", + "max": null, + "min": null, + "name": "quota", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2063623452", + "max": 0, + "min": 0, + "name": "status", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file507207115", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "avatar_file", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation2809058197", + "maxSelect": 1, + "minSelect": 0, + "name": "user_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1196309394", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "LessonsCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1137421714", + "max": 0, + "min": 0, + "name": "cat_image_url", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2328411368", + "hidden": false, + "id": "relation3455582614", + "maxSelect": 1, + "minSelect": 0, + "name": "lesson_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "convertURLs": false, + "hidden": false, + "id": "editor1843675174", + "maxSize": 0, + "name": "description", + "presentable": false, + "required": false, + "system": false, + "type": "editor" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2328411368", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "LessonsTypes", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2363381545", + "max": 0, + "min": 0, + "name": "type", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "date1542800728", + "max": "", + "min": "", + "name": "field", + "presentable": false, + "required": false, + "system": false, + "type": "date" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_4061499106", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizCRCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3141885671", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizCRQuestions", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2416551515", + "max": 0, + "min": 0, + "name": "question_fh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2814132303", + "max": 0, + "min": 0, + "name": "question_sh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1249130051", + "max": 0, + "min": 0, + "name": "modal_ans", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_4061499106", + "hidden": false, + "id": "relation1827623476", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "json3493198471", + "maxSize": 0, + "name": "options", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3571292172", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2034676914", + "max": 0, + "min": 0, + "name": "cat_image", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_96745150", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizConnectives", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2416551515", + "max": 0, + "min": 0, + "name": "question_fh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2814132303", + "max": 0, + "min": 0, + "name": "question_sh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1249130051", + "max": 0, + "min": 0, + "name": "modal_ans", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_342761728", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 999, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_342761728", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizConnectivesCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3639453778", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizLPCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2560465762", + "max": 0, + "min": 0, + "name": "slug", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "convertURLs": false, + "hidden": false, + "id": "editor1843675174", + "maxSize": 0, + "name": "description", + "presentable": false, + "required": false, + "system": false, + "type": "editor" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_742947356", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizLPQuestions", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3639453778", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2560465762", + "max": 0, + "min": 0, + "name": "slug", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "convertURLs": false, + "hidden": false, + "id": "editor1843675174", + "maxSize": 0, + "name": "description", + "presentable": false, + "required": false, + "system": false, + "type": "editor" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2511066072", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizListenings", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3571292172", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 999, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_84667061", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizMFCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3346420851", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizMFQuestions", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3690399444", + "max": 0, + "min": 0, + "name": "word_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_84667061", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2936646783", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizMatchings", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3690399444", + "max": 0, + "min": 0, + "name": "word_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3571292172", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 999, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1305841361", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "UserMetas", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4192936109", + "max": 0, + "min": 0, + "name": "helloworld", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "json3622966325", + "maxSize": 0, + "name": "meta", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation2809058197", + "maxSelect": 1, + "minSelect": 0, + "name": "user_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2744374011", + "max": 0, + "min": 0, + "name": "state", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file376926767", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "avatar", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1466534506", + "max": 0, + "min": 0, + "name": "role", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1638686383", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "Vocabularies", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "file3309110367", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3690399444", + "max": 0, + "min": 0, + "name": "word_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text412313404", + "max": 0, + "min": 0, + "name": "sample_e", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4059087369", + "max": 0, + "min": 0, + "name": "sample_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1430376151", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text105650625", + "max": 0, + "min": 0, + "name": "category", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2328411368", + "hidden": false, + "id": "relation808508980", + "maxSelect": 1, + "minSelect": 0, + "name": "lesson_type_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_4275539003", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "name": "_authOrigins", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4228609354", + "max": 0, + "min": 0, + "name": "fingerprint", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_authOrigins_unique_pairs` ON `_authOrigins` (collectionRef, recordRef, fingerprint)" + ], + "system": true + }, + { + "id": "pbc_2281828961", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "name": "_externalAuths", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2462348188", + "max": 0, + "min": 0, + "name": "provider", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1044722854", + "max": 0, + "min": 0, + "name": "providerId", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_externalAuths_record_provider` ON `_externalAuths` (collectionRef, recordRef, provider)", + "CREATE UNIQUE INDEX `idx_externalAuths_collection_provider` ON `_externalAuths` (collectionRef, provider, providerId)" + ], + "system": true + }, + { + "id": "pbc_2279338944", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "_mfas", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1582905952", + "max": 0, + "min": 0, + "name": "method", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE INDEX `idx_mfas_collectionRef_recordRef` ON `_mfas` (collectionRef,recordRef)" + ], + "system": true + }, + { + "id": "pbc_1638494021", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "_otps", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 8, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 0, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text3866985172", + "max": 0, + "min": 0, + "name": "sentTo", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE INDEX `idx_otps_collectionRef_recordRef` ON `_otps` (collectionRef, recordRef)" + ], + "system": true + }, + { + "id": "pbc_1509025625", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "billingAddress", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1400097126", + "max": 0, + "min": 0, + "name": "country", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2744374011", + "max": 0, + "min": 0, + "name": "state", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text760939060", + "max": 0, + "min": 0, + "name": "city", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4114525948", + "max": 0, + "min": 0, + "name": "zipCode", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3620973610", + "max": 0, + "min": 0, + "name": "line1", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1322974608", + "max": 0, + "min": 0, + "name": "line2", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_123408445", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "helloworlds", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text907060870", + "max": 0, + "min": 0, + "name": "hello", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2109205374", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "t1", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text907060870", + "max": 0, + "min": 0, + "name": "hello", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "file2313559263", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "test_file", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + } + ], + "indexes": [], + "system": false + } +] +
+ +
diff --git a/002_source/cms/src/hooks/_GUIDELINE.md b/002_source/cms/src/hooks/_GUIDELINE.md new file mode 100644 index 0000000..5d048d5 --- /dev/null +++ b/002_source/cms/src/hooks/_GUIDELINE.md @@ -0,0 +1,32 @@ +# GUIDELINE + +## Files highlight + +- use-dialog (to control the open, close of dialog) + +## Example + +```tsx +import * as React from 'react'; + +import type { UserContextValue } from '@/contexts/auth/types'; + +// work with src/contexts/auth/user-context.ts, context and provider defined there +import { UserContext } from '@/contexts/auth/user-context'; + +export function useUser(): UserContextValue { + const context = React.useContext(UserContext); + + if (!context) { + // RULES: error handline placeholder + throw new Error('useUser must be used within a UserProvider'); + } + + return context; +} +``` + +## Rules + +When you add files in this folder, +please remember add the corressponding information to this file