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')}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+```
+
+## 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')}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+
+
+
+'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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+````
+
+## 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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+````
+
+## 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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+
+
+
+'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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+
+
+
+'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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+
+
+
+'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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+
+
+
+'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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+```
+
+## 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('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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+```
+
+## 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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+
+
+
+'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('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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+
+
+
+'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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+
+
+
+'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('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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+
+
+
+'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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+```
+
+## 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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+```
+
+## 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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+
+
+
+'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('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}
+
+
+
+
+ }
+ variant="contained"
+ >
+ {t('list.action')}
+
+
+ >
+ );
+}
+
+
+
+'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('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 (
-
-
-
-
-
-
-
- MV
-
-
-
- Miron Vitold
- }
- label="Active"
- size="small"
- variant="outlined"
- />
-
-
- miron.vitold@domain.com
-
-
-
-
- } variant="contained">
- Action
-
-
-
-
-
-
-
-
-
-
-
- }
- 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 (
-
-
-
-
-
- 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
-
-
- }
- variant="contained"
- >
- Add
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-// 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 (
-
-
-
-
-
-
-
- MV
-
-
-
- Miron Vitold
- }
- label="Active"
- size="small"
- variant="outlined"
- />
-
-
- miron.vitold@domain.com
-
-
-
-
- } variant="contained">
- Action
-
-
-
-
-
-
-
-
-
-
-
- }
- 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 (
-
-
-
-
-
- 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
-
-
- }
- variant="contained"
- >
- Add
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-// 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 (
-
- );
-}
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"
- />
-
-
-
-
-
- } variant="contained">
- Send email
-
-
-
-
-
- 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 ? : }
- }>
- Edit
-
-
-
-
-
- );
-}
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 (
-
- );
-}
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"
- />
-
-
-
-
-
- } variant="contained">
- Send email
-
-
-
-
-
- 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 ? : }
- }>
- Edit
-
-
-
-
-
- );
-}
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 (
+
+ );
+}
+````
+
+## 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 (
+
+ );
+}
+````
+
+## 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"
+ />
+
+
+
+
+
+ } variant="contained">
+ Send email
+
+
+
+
+
+ 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 ? : }
+ }>
+ Edit
+
+
+
+
+
+ );
+}
+````
+
+## 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 (
+
+ );
+}
+````
+
+## 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 (
+
+ );
+}
+````
+
+## 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"
+ />
+
+
+
+
+
+ } variant="contained">
+ Send email
+
+
+
+
+
+ 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 ? : }
+ }>
+ Edit
+
+
+
+
+
+ );
+}
+````
+
+## 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 (
+
+ );
+}
+
+
+
+'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 (
+
+ );
+}
+
+
+
+'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"
+ />
+
+
+
+
+
+ } variant="contained">
+ Send email
+
+
+
+
+
+ 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 ? : }
+ }>
+ Edit
+
+
+
+
+
+ );
+}
+
+
+
+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 (
+
+ );
+}
+
+
+
+'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 (
+
+ );
+}
+
+
+
+'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"
+ />
+
+
+
+
+
+ } variant="contained">
+ Send email
+
+
+
+
+
+ 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 ? : }
+ }>
+ Edit
+
+
+
+
+
+ );
+}
+
+
+
+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 (
+
+ );
+}
+````
+
+## 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 (
+
+ );
+}
+````
+
+## 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 (
+
+ );
+}
+````
+
+## 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 (
+
+ );
+}
+````
+
+## 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"
+ />
+
+
+
+
+
+ } variant="contained">
+ Send email
+
+
+
+
+
+ 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 ? : }
+ }>
+ Edit
+
+
+
+
+
+ );
+}
+````
+
+## 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 (
+
+ );
+}
+````
+
+## 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 (
+
+ );
+}
+````
+
+## 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"
+ />
+
+
+
+
+
+ } variant="contained">
+ Send email
+
+
+
+
+
+ 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 ? : }
+ }>
+ Edit
+
+
+
+
+
+ );
+}
+````
+
+## 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 (
+
+ );
+}
+
+
+
+'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 (
+