update build ok,

This commit is contained in:
louiscklaw
2025-04-20 02:00:25 +08:00
parent 6b19656833
commit b963a85cc6
30 changed files with 1133 additions and 695 deletions

View File

@@ -11,6 +11,7 @@ dashboard.lessonCategories.edit.name
dashboard.lessonCategories.edit.type
```
---
please read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lesson_category/lesson-category-edit-form.tsx`
@@ -20,3 +21,19 @@ and update `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/letter
please refactor `common.json` to smaller translation files, thanks
e.g. `lessonTypes` -> `lesson_type`
---
Hi, i want you to help merge two translation files.
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/public/locales/dev`
I want you to merge the content
from `<base_dir>/lesson_type.json` (source file)
to `<base_dir>/listening_practice.json` (dest file)
please extract , link up and remember the document properties
(e.g. types, functions, variables, constants, etc)
update the variables and properties of dest file to reflect `listening practice categories`/`lp_categories`

View File

@@ -1,5 +1,36 @@
{
"hello": "world",
"add": "新加",
"listening-practice": "聽力練習"
}
"add": "新增",
"listening-practice": "聽力練習",
"list": {
"title": "聽力練習分類列表",
"empty": "沒有找到聽力練習分類",
"action": "動作?",
"basic-details": "基本資料"
},
"helloworld": "listening_practice",
"title": "聽力練習分類",
"type": "聽力練習分類",
"error": {
"invalid": "無效的聽力練習分類",
"not_found": "未找到聽力練習分類"
},
"create": {
"title": "創建聽力練習分類",
"success": "聽力練習分類創建成功",
"error": "創建聽力練習分類時出錯"
},
"edit": {
"title": "編輯聽力練習分類",
"success": "聽力練習分類更新成功",
"error": "更新聽力練習分類時出錯"
},
"delete": {
"title": "刪除聽力練習分類",
"confirm": "確定要刪除聽力練習分類嗎?",
"success": "聽力練習分類刪除成功",
"error": "刪除聽力練習分類時出錯"
},
"view": {
"title": "查看聽力練習分類詳情"
}
}

View File

@@ -0,0 +1,79 @@
'use client';
import * as React from 'react';
import { useParams, 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 { paths } from '@/paths';
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();
const router = useRouter();
return (
<Card>
<CardHeader
action={
<IconButton
onClick={() => {
handleEditClick();
}}
>
<PencilSimpleIcon />
</IconButton>
}
avatar={
<Avatar>
<UserIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('list.basic-details')}
/>
<PropertyList divider={<Divider />} orientation="vertical" sx={{ '--PropertyItem-padding': '12px 24px' }}>
{(
[
{ key: 'Customer ID', value: <Chip label="USR-001" size="small" variant="soft" /> },
{ 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: (
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" />
<Typography color="text.secondary" variant="body2">
50%
</Typography>
</Stack>
),
},
] satisfies { key: string; value: React.ReactNode }[]
).map(
(item): React.JSX.Element => (
<PropertyItem key={item.key} name={item.key} value={item.value} />
)
)}
</PropertyList>
</Card>
);
}

View File

@@ -0,0 +1,14 @@
'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;

View File

@@ -0,0 +1,80 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import getLessonCategoryById from '@/db/LessonCategories/GetById';
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 type { RecordModel } from 'pocketbase';
import { useTranslation } from 'react-i18next';
import type { Address } from '@/types/Address';
import { LpCategory } from '@/types/LpCategory';
import { paths } from '@/paths';
import { dayjs } from '@/lib/dayjs';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import LpCategoryDefaultValue, { defaultLpCategory } from '@/components/dashboard/lp_categories/_constants';
import { Notifications } from '@/components/dashboard/lp_categories/notifications';
import { Payments } from '@/components/dashboard/lp_categories/payments';
import { ShippingAddress } from '@/components/dashboard/lp_categories/shipping-address';
import { SampleAddresses } from './SampleAddresses';
import { SampleNotifications } from './SampleNotifications';
import { SamplePayments } from './SamplePayments';
export default function SampleAddressCard(): React.JSX.Element {
const { t } = useTranslation();
return (
<Card>
<CardHeader
action={
<Button color="secondary" startIcon={<PlusIcon />}>
{t('list.add')}
</Button>
}
avatar={
<Avatar>
<HouseIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('list.shipping-addresses')}
/>
<CardContent>
<Grid container spacing={3}>
{(SampleAddresses satisfies Address[]).map((address) => (
<Grid key={address.id} md={6} xs={12}>
<ShippingAddress address={address} />
</Grid>
))}
</Grid>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,95 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import getLessonCategoryById from '@/db/LessonCategories/GetById';
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 type { RecordModel } from 'pocketbase';
import { useTranslation } from 'react-i18next';
import type { Address } from '@/types/Address';
import { LpCategory } from '@/types/LpCategory';
import { paths } from '@/paths';
import { dayjs } from '@/lib/dayjs';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import LpCategoryDefaultValue, { defaultLpCategory } from '@/components/dashboard/lp_categories/_constants';
import { Notifications } from '@/components/dashboard/lp_categories/notifications';
import { Payments } from '@/components/dashboard/lp_categories/payments';
import { ShippingAddress } from '@/components/dashboard/lp_categories/shipping-address';
import SampleAddressCard from './SampleAddressCard';
import { SampleAddresses } from './SampleAddresses';
import { SampleNotifications } from './SampleNotifications';
import { SamplePayments } from './SamplePayments';
export default function SamplePaymentCard(): React.JSX.Element {
const { t } = useTranslation();
return (
<>
<Payments ordersValue={2069.48} payments={SamplePayments} refundsValue={324.5} totalOrders={5} />
<Card>
<CardHeader
action={
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
{t('list.edit')}
</Button>
}
avatar={
<Avatar>
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('list.billing-details')}
/>
<CardContent>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<PropertyList divider={<Divider />} 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 => (
<PropertyItem key={item.key} name={item.key} value={item.value} />
)
)}
</PropertyList>
</Card>
</CardContent>
</Card>
</>
);
}

View File

@@ -0,0 +1,41 @@
'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 (
<Card>
<CardHeader
avatar={
<Avatar>
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('list.security')}
/>
<CardContent>
<Stack spacing={1}>
<div>
<Button color="error" variant="contained">
{t('Delete account')}
</Button>
</div>
<Typography color="text.secondary" variant="body2">
{t('a-deleted-customer-cannot-be-restored-all-data-will-be-permanently-removed')}
</Typography>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,44 @@
'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 (
<>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
<Avatar src="/assets/avatar-1.png" sx={{ '--Avatar-size': '64px' }}>
empty
</Avatar>
<div>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
<Typography variant="h4">{t('list.customer-name')}</Typography>
<Chip
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
label={t('list.active')}
size="small"
variant="outlined"
/>
</Stack>
<Typography color="text.secondary" variant="body1">
{t('list.customer-email')}
</Typography>
</div>
</Stack>
<div>
<Button endIcon={<CaretDownIcon />} variant="contained">
{t('list.action')}
</Button>
</div>
</>
);
}

View File

@@ -0,0 +1,43 @@
'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(),
},
];

View File

@@ -3,59 +3,42 @@
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
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 type { RecordModel } from 'pocketbase';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { dayjs } from '@/lib/dayjs';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultLessonType, LessonTypeDefaultValue } from '@/components/dashboard/lesson_type/_constants';
// import { getLessonTypeById } from '@/components/dashboard/lesson_type/http-actions';
// import { LessonTypeDefaultValue, type LessonType } from '@/components/dashboard/lesson_type/ILessonType';
// import { defaultLessonType } from '@/components/dashboard/lesson_type/interfaces';
import { Notifications } from '@/components/dashboard/lesson_type/notifications';
import { Payments } from '@/components/dashboard/lesson_type/payments';
import type { Address } from '@/components/dashboard/lesson_type/shipping-address';
import { ShippingAddress } from '@/components/dashboard/lesson_type/shipping-address';
import { type LessonType } from '@/components/dashboard/lesson_type/types';
import FormLoading from '@/components/loading';
import BasicDetailCard from '../../Sample/BasicDetailCard';
import SampleAddressCard from '../../Sample/SampleAddressCard';
import { SampleNotifications } from '../../Sample/SampleNotifications';
import SamplePaymentCard from '../../Sample/SamplePaymentCard';
import SampleSecurityCard from '../../Sample/SampleSecurityCard';
import SampleTitleCard from '../../Sample/SampleTitleCard';
export default function Page(): React.JSX.Element {
const { t } = useTranslation(['common', 'lesson_type']);
const { t } = useTranslation(['lesson_type']);
const router = useRouter();
//
const { type_id: typeId } = useParams<{ type_id: string }>();
//
const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState<boolean>(false);
const [errorDetails, setErrorDetails] = React.useState('');
//
const [showLessonType, setShowLessonType] = React.useState<LessonType>(LessonTypeDefaultValue);
@@ -73,7 +56,7 @@ export default function Page(): React.JSX.Element {
.catch((err) => {
logger.error(err);
toast(t('dashboard.lessonTypes.list.error'));
setErrorDetails(err);
setShowError(true);
})
.finally(() => {
@@ -89,7 +72,7 @@ export default function Page(): React.JSX.Element {
<ErrorDisplay
message={t('error.unable-to-process-request', { ns: 'common' })}
code="500"
details={t('error.detailed-error-information', { ns: 'common' })}
details={JSON.stringify(errorDetails, null, 2)}
/>
);
@@ -113,261 +96,25 @@ export default function Page(): React.JSX.Element {
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
{t('list.title', { ns: 'lesson_type' })}
{t('list.title')}
</Link>
</div>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
<Avatar src="/assets/avatar-1.png" sx={{ '--Avatar-size': '64px' }}>
empty
</Avatar>
<div>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
<Typography variant="h4">{showLessonType.name}</Typography>
<Chip
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
label={showLessonType.visible}
size="small"
variant="outlined"
/>
</Stack>
<Typography color="text.secondary" variant="body1">
{showLessonType.id}
</Typography>
</div>
</Stack>
<div>
<Button endIcon={<CaretDownIcon />} variant="contained">
Action
</Button>
</div>
<SampleTitleCard />
</Stack>
</Stack>
<Grid container spacing={4}>
<Grid lg={4} xs={12}>
<Stack spacing={4}>
<Card>
<CardHeader
action={
<IconButton
onClick={() => {
handleEditClick();
}}
>
<PencilSimpleIcon />
</IconButton>
}
avatar={
<Avatar>
<UserIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('basic-details', { ns: 'lesson_type' })}
/>
<PropertyList
divider={<Divider />}
orientation="vertical"
sx={{ '--PropertyItem-padding': '12px 24px' }}
>
{(
[
{ key: 'Customer ID', value: <Chip label={showLessonType.id} size="small" variant="soft" /> },
{ key: 'Name', value: showLessonType.name },
{ key: 'Type', value: showLessonType.type },
{ key: 'Pos', value: showLessonType.pos },
{
key: 'Visible',
value: (
<Chip
//
label={showLessonType.visible}
size="small"
variant="soft"
/>
),
},
{
key: 'Quota',
value: (
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" />
<Typography color="text.secondary" variant="body2">
50%
</Typography>
</Stack>
),
},
] satisfies { key: string; value: React.ReactNode }[]
).map(
(item): React.JSX.Element => (
<PropertyItem key={item.key} name={item.key} value={item.value} />
)
)}
</PropertyList>
</Card>
<Card>
<CardHeader
avatar={
<Avatar>
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('security', { ns: 'lesson_type' })}
/>
<CardContent>
<Stack spacing={1}>
<div>
<Button color="error" variant="contained">
Delete account
</Button>
</div>
<Typography color="text.secondary" variant="body2">
A deleted lesson type cannot be restored. All data will be permanently removed.
</Typography>
</Stack>
</CardContent>
</Card>
<BasicDetailCard lpCatId={showLessonType.id} handleEditClick={handleEditClick} />
<SampleSecurityCard />
</Stack>
</Grid>
<Grid lg={8} xs={12}>
<Stack spacing={4}>
<Payments
ordersValue={2069.48}
payments={[
{
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(),
},
]}
refundsValue={324.5}
totalOrders={5}
/>
<Card>
<CardHeader
action={
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
Edit
</Button>
}
avatar={
<Avatar>
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('billing-details', { ns: 'lesson_type' })}
/>
<CardContent>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<PropertyList divider={<Divider />} 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 => (
<PropertyItem key={item.key} name={item.key} value={item.value} />
)
)}
</PropertyList>
</Card>
</CardContent>
</Card>
<Card>
<CardHeader
action={
<Button color="secondary" startIcon={<PlusIcon />}>
Add
</Button>
}
avatar={
<Avatar>
<HouseIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('shipping-addresses', { ns: 'lesson_type' })}
/>
<CardContent>
<Grid container spacing={3}>
{(
[
{
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) => (
<Grid key={address.id} md={6} xs={12}>
<ShippingAddress address={address} />
</Grid>
))}
</Grid>
</CardContent>
</Card>
<Notifications
notifications={[
{
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(),
},
]}
/>
<SamplePaymentCard />
<SampleAddressCard />
<Notifications notifications={SampleNotifications} />
</Stack>
</Grid>
</Grid>

View File

@@ -40,12 +40,9 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState<boolean>(false);
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
//
const [f, setF] = React.useState<LessonType[]>([]);
const [currentPage, setCurrentPage] = React.useState<number>(0);
//
const [recordCount, setRecordCount] = React.useState<number>(0);
const [listOption, setListOption] = React.useState({});
const [listSort, setListSort] = React.useState({});
@@ -143,10 +140,15 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
filters={{ email, phone, status, name, visible, type }}
fullData={lessonTypesData}
sortDir={sortDir}
//
/>
<Divider />
<Box sx={{ overflowX: 'auto' }}>
<LessonTypesTable reloadRows={reloadRows} rows={f} />
<LessonTypesTable
reloadRows={reloadRows}
rows={f}
//
/>
</Box>
<Divider />
<LessonTypesPagination

View File

@@ -1,236 +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 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 { useTranslation } from 'react-i18next';
import type { Address } from '@/types/Address';
import { paths } from '@/paths';
import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list';
import { Notifications } from '@/components/dashboard/lp_categories/notifications';
import { Payments } from '@/components/dashboard/lp_categories/payments';
import { ShippingAddress } from '@/components/dashboard/lp_categories/shipping-address';
import { SampleAddresses } from './SampleAddresses';
import { SampleNotifications } from './SampleNotifications';
import { SamplePayments } from './SamplePayments';
export default function Page(): React.JSX.Element {
const { t } = useTranslation();
return (
<Box
sx={{
maxWidth: 'var(--Content-maxWidth)',
m: 'var(--Content-margin)',
p: 'var(--Content-padding)',
width: 'var(--Content-width)',
}}
>
<Stack spacing={4}>
<Stack spacing={3}>
<div>
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.lp_categories.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
{t('Customers')}
</Link>
</div>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
<Avatar src="/assets/avatar-1.png" sx={{ '--Avatar-size': '64px' }}>
MV
</Avatar>
<div>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
<Typography variant="h4">{t('Customer name')}</Typography>
<Chip
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
label={t('Active')}
size="small"
variant="outlined"
/>
</Stack>
<Typography color="text.secondary" variant="body1">
{t('Customer email')}
</Typography>
</div>
</Stack>
<div>
<Button endIcon={<CaretDownIcon />} variant="contained">
{t('Action')}
</Button>
</div>
</Stack>
</Stack>
<Grid container spacing={4}>
<Grid lg={4} xs={12}>
<Stack spacing={4}>
<Card>
<CardHeader
action={
<IconButton>
<PencilSimpleIcon />
</IconButton>
}
avatar={
<Avatar>
<UserIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('Basic details')}
/>
<PropertyList
divider={<Divider />}
orientation="vertical"
sx={{ '--PropertyItem-padding': '12px 24px' }}
>
{(
[
{ key: t('Customer ID'), value: <Chip label={t('USR-001')} size="small" variant="soft" /> },
{ key: t('Name'), value: t('Customer name') },
{ key: t('Email'), value: t('Customer email') },
{ key: t('Phone'), value: t('Customer phone') },
{ key: t('Company'), value: t('Company name') },
{
key: t('Quota'),
value: (
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" />
<Typography color="text.secondary" variant="body2">
50%
</Typography>
</Stack>
),
},
] satisfies { key: string; value: React.ReactNode }[]
).map(
(item): React.JSX.Element => (
<PropertyItem key={item.key} name={item.key} value={item.value} />
)
)}
</PropertyList>
</Card>
<Card>
<CardHeader
avatar={
<Avatar>
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('Security')}
/>
<CardContent>
<Stack spacing={1}>
<div>
<Button color="error" variant="contained">
{t('Delete account')}
</Button>
</div>
<Typography color="text.secondary" variant="body2">
{t('A deleted customer cannot be restored. All data will be permanently removed.')}
</Typography>
</Stack>
</CardContent>
</Card>
</Stack>
</Grid>
<Grid lg={8} xs={12}>
<Stack spacing={4}>
<Payments ordersValue={2069.48} payments={SamplePayments} refundsValue={324.5} totalOrders={5} />
<Card>
<CardHeader
action={
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
{t('Edit')}
</Button>
}
avatar={
<Avatar>
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('Billing details')}
/>
<CardContent>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<PropertyList divider={<Divider />} 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 => (
<PropertyItem key={item.key} name={item.key} value={item.value} />
)
)}
</PropertyList>
</Card>
</CardContent>
</Card>
<Card>
<CardHeader
action={
<Button color="secondary" startIcon={<PlusIcon />}>
{t('Add')}
</Button>
}
avatar={
<Avatar>
<HouseIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title={t('Shipping addresses')}
/>
<CardContent>
<Grid container spacing={3}>
{(SampleAddresses satisfies Address[]).map((address) => (
<Grid key={address.id} md={6} xs={12}>
<ShippingAddress address={address} />
</Grid>
))}
</Grid>
</CardContent>
</Card>
<Notifications notifications={SampleNotifications} />
</Stack>
</Grid>
</Grid>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,121 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import getQuizListeningById from '@/db/QuizListenings/GetById';
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 type { LpCategory } from '@/types/LpCategory';
import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultLpCategory, LpCategoryDefaultValue } from '@/components/dashboard/lp_categories/_constants';
import { Notifications } from '@/components/dashboard/lp_categories/notifications';
import FormLoading from '@/components/loading';
import BasicDetailCard from '../../Sample/BasicDetailCard';
import SampleAddressCard from '../../Sample/SampleAddressCard';
import { SampleNotifications } from '../../Sample/SampleNotifications';
import SamplePaymentCard from '../../Sample/SamplePaymentCard';
import SampleSecurityCard from '../../Sample/SampleSecurityCard';
import SampleTitleCard from '../../Sample/SampleTitleCard';
export default function Page(): React.JSX.Element {
const { t } = useTranslation(['listening_practice']);
const router = useRouter();
//
const { lp_cat_id: lpCatId } = useParams<{ lp_cat_id: string }>();
//
const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState<boolean>(false);
const [errorDetails, setErrorDetails] = React.useState('');
//
const [showLessonType, setShowLessonType] = React.useState<LpCategory>(LpCategoryDefaultValue.default);
function handleEditClick(): void {
router.push(paths.dashboard.lp_categories.edit(lpCatId));
}
React.useEffect(() => {
getQuizListeningById(lpCatId)
.then((model: RecordModel) => {
setShowLessonType({ ...defaultLpCategory, ...model });
})
.catch((err) => {
logger.error(err);
toast(t('list.error'));
setErrorDetails(err);
setShowError(true);
})
.finally(() => {
setShowLoading(false);
});
}, [lpCatId]);
if (showLoading) return <FormLoading />;
if (showError)
return (
<ErrorDisplay
message={t('error.unable-to-process-request', { ns: 'common' })}
code="500"
details={JSON.stringify(errorDetails, null, 2)}
/>
);
return (
<Box
sx={{
maxWidth: 'var(--Content-maxWidth)',
m: 'var(--Content-margin)',
p: 'var(--Content-padding)',
width: 'var(--Content-width)',
}}
>
<Stack spacing={4}>
<Stack spacing={3}>
<div>
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.lp_categories.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
{t('list.title')}
</Link>
</div>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
<SampleTitleCard />
</Stack>
</Stack>
<Grid container spacing={4}>
<Grid lg={4} xs={12}>
<Stack spacing={4}>
<BasicDetailCard lpCatId={showLessonType.id} handleEditClick={handleEditClick} />
<SampleSecurityCard />
</Stack>
</Grid>
<Grid lg={8} xs={12}>
<Stack spacing={4}>
<SamplePaymentCard />
<SampleAddressCard />
<Notifications notifications={SampleNotifications} />
</Stack>
</Grid>
</Grid>
</Stack>
</Box>
);
}

View File

@@ -2,35 +2,106 @@
import * as React from 'react';
import { useRouter } from 'next/navigation';
import getAllQuizListenings from '@/db/QuizListenings/GetAll';
import listWithOption from '@/db/QuizListenings/ListWithOption';
import { LoadingButton } from '@mui/lab';
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 type { ListResult, RecordModel } from 'pocketbase';
import { useTranslation } from 'react-i18next';
import type { LpCategory } from '@/types/LpCategory';
import { paths } from '@/paths';
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 { LpCategories } from './lp-categories';
import FormLoading from '@/components/loading';
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['listening_practice']);
const { email, phone, sortDir, status } = searchParams;
const { email, phone, sortDir, status, name, visible, type } = searchParams;
const router = useRouter();
const [lpCategoriesData, setLpCategoriesData] = React.useState<LpCategory[]>([]);
//
const sortedCustomers = applySort(LpCategories, sortDir);
const lpCategories = applyFilters(sortedCustomers, { email, phone, status });
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState<boolean>(false);
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
const [f, setF] = React.useState<LpCategory[]>([]);
const [currentPage, setCurrentPage] = React.useState<number>(0);
const [recordCount, setRecordCount] = React.useState<number>(0);
const [listOption, setListOption] = React.useState({});
const [listSort, setListSort] = React.useState({});
React.useEffect(() => {}, []);
//
const reloadRows = async (): Promise<void> => {
try {
const models: ListResult<RecordModel> = await listWithOption({
currentPage: currentPage + 1,
rowsPerPage,
listOption,
});
const { items, totalItems } = models;
const tempLpCategories: LpCategory[] = items.map((lt) => {
return { ...defaultLpCategory, ...lt };
});
setLpCategoriesData(tempLpCategories);
setRecordCount(totalItems);
setF(tempLpCategories);
} catch (error) {
//
} finally {
setShowLoading(false);
}
};
React.useEffect(() => {
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}%"`);
}
setListOption({
filter: tempFilter.join(' && '),
sort: tempSortDir,
//
});
}, [visible, sortDir, name, type]);
if (f.length === 0 || showLoading) return <FormLoading />;
if (showError)
return (
<ErrorDisplay message={t('unable-to-process-request')} code="500" details={t('detailed-error-information')} />
);
return (
<Box
@@ -44,29 +115,45 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
<Stack spacing={4}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
<Box sx={{ flex: '1 1 auto' }}>
<Typography variant="h4">{t('listening-practice')}</Typography>
<Typography variant="h4">{t('list.title')}</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button
onClick={() => {
<LoadingButton
loading={isLoadingAddPage}
onClick={(): void => {
setIsLoadingAddPage(true);
router.push(paths.dashboard.lp_categories.create);
}}
startIcon={<PlusIcon />}
variant="contained"
>
{t('add')}
</Button>
</LoadingButton>
</Box>
</Stack>
<LpCategoriesSelectionProvider LpCategories={lpCategories}>
<LpCategoriesSelectionProvider LpCategories={f}>
<Card>
<LpCategoriesFilters filters={{ email, phone, status }} sortDir={sortDir} />
<LpCategoriesFilters
filters={{ email, phone, status }}
fullData={f}
sortDir={sortDir}
//
/>
<Divider />
<Box sx={{ overflowX: 'auto' }}>
<LpCategoriesTable rows={lpCategories} />
<LpCategoriesTable
rows={f}
//
/>
</Box>
<Divider />
<LpCategoriesPagination count={lpCategories.length + 100} page={0} />
<LpCategoriesPagination
count={recordCount}
page={currentPage}
rowsPerPage={rowsPerPage}
setPage={setCurrentPage}
setRowsPerPage={setRowsPerPage}
/>
</Card>
</LpCategoriesSelectionProvider>
</Stack>
@@ -86,7 +173,7 @@ function applySort(row: LpCategory[], sortDir: 'asc' | 'desc' | undefined): LpCa
});
}
function applyFilters(row: LpCategory[], { email, phone, status }: Filters): LpCategory[] {
function applyFilters(row: LpCategory[], { email, phone, status, name, visible }: Filters): LpCategory[] {
return row.filter((item) => {
if (email) {
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
@@ -106,10 +193,31 @@ function applyFilters(row: LpCategory[], { email, phone, status }: Filters): LpC
}
}
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 };
searchParams: {
email?: string;
phone?: string;
sortDir?: 'asc' | 'desc';
status?: string;
name?: string;
visible?: string;
type?: string;
//
};
}

View File

@@ -39,9 +39,11 @@ function ErrorDetails({ details }: { details: string }): React.JSX.Element {
<Collapse in={expanded}>
<Box sx={{ mt: 1, p: 1, bgcolor: 'background.paper' }}>
<Typography variant="body2" component="pre" sx={{ whiteSpace: 'pre-wrap' }}>
{details}
</Typography>
<pre>
<Typography variant="body2" component="pre" sx={{ whiteSpace: 'pre-wrap' }}>
{details}
</Typography>
</pre>
</Box>
</Collapse>
</Box>

View File

@@ -24,9 +24,8 @@ import { pb } from '@/lib/pb';
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
import { Option } from '@/components/core/option';
// import type { LessonType } from './ILessonType';
import { useLessonTypesSelection } from './lesson-types-selection-context';
import { LessonType } from './types';
import type { LessonType } from './types';
export interface Filters {
email?: string;
@@ -61,18 +60,6 @@ export function LessonTypesFilters({
const selection = useLessonTypesSelection();
function getVisible(): number {
return fullData.reduce((count, item: LessonType) => {
return item.visible === 'visible' ? count + 1 : count;
}, 0);
}
function getHidden(): number {
return fullData.reduce((count, item: LessonType) => {
return item.visible === 'hidden' ? count + 1 : count;
}, 0);
}
// The tabs should be generated using API data.
const tabs = [
{ label: t('All'), value: '', count: totalCount },
@@ -195,7 +182,13 @@ export function LessonTypesFilters({
return (
<div>
<Tabs onChange={handleVisibleChange} sx={{ px: 3 }} value={visible ?? ''} variant="scrollable">
<Tabs
onChange={handleVisibleChange}
sx={{ px: 3 }}
value={visible ?? ''}
variant="scrollable"
//
>
{tabs.map((tab) => (
<Tab
icon={<Chip label={tab.count} size="small" variant="soft" />}

View File

@@ -0,0 +1,41 @@
import { NO_NUM, NO_VALUE } from '@/constants';
import type { RecordModel } from 'pocketbase';
import { CreateForm, LpCategory } from '@/types/LpCategory';
import { dayjs } from '@/lib/dayjs';
// import type { CreateForm, LpCategory } from './types';
export const LpCategoryCreateFormDefault: CreateForm = {
name: '',
description: '',
isActive: true,
order: 1,
imageUrl: '',
};
export const defaultLpCategory: LpCategory = {
id: '',
name: '',
description: '',
isActive: true,
order: 1,
imageUrl: '',
createdAt: dayjs().toDate(),
updatedAt: dayjs().toDate(),
visible: 'active',
status: '',
};
export const emptyLpCategory: LpCategory = {
...defaultLpCategory,
isActive: false,
};
export const LpCategoryDefaultValue = {
createForm: LpCategoryCreateFormDefault,
default: defaultLpCategory,
empty: emptyLpCategory,
};
export default LpCategoryDefaultValue;

View File

@@ -3,6 +3,8 @@
import * as React from 'react';
import { useRouter } from 'next/navigation';
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';
@@ -16,41 +18,56 @@ import Tabs from '@mui/material/Tabs';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import type { LpCategory } from '@/types/LpCategory';
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';
// The tabs should be generated using API data.
const tabs1 = [
{ 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;
name?: string;
visible?: string;
type?: string;
}
export type SortDir = 'asc' | 'desc';
export interface CustomersFiltersProps {
export interface LpCategoriesFiltersProps {
filters?: Filters;
sortDir?: SortDir;
fullData: LpCategory[];
}
export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: CustomersFiltersProps): React.JSX.Element {
export function LpCategoriesFilters({
filters = {},
sortDir = 'desc',
fullData,
}: LpCategoriesFiltersProps): React.JSX.Element {
const { t } = useTranslation();
const { email, phone, status } = filters;
const { email, phone, status, name, visible, type } = filters;
const [totalCount, setTotalCount] = React.useState<number>(0);
const [visibleCount, setVisibleCount] = React.useState<number>(0);
const [hiddenCount, setHiddenCount] = React.useState<number>(0);
const router = useRouter();
const selection = useLpCategoriesSelection();
// 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();
@@ -71,6 +88,18 @@ export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: Customer
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);
}
router.push(`${paths.dashboard.lp_categories.list}?${searchParams.toString()}`);
},
[router]
@@ -87,6 +116,27 @@ export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: Customer
[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);
@@ -108,28 +158,35 @@ export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: Customer
[updateSearchParams, filters]
);
const [allCount, setAllCount] = React.useState<number>(0);
React.useEffect(() => {
async function fetchAllCount(): Promise<void> {
setAllCount(await GetAllCount());
}
const fetchCount = async (): Promise<void> => {
try {
const tc = await GetAllCount();
setTotalCount(tc);
void fetchAllCount();
const vc = await GetVisibleCount();
setVisibleCount(vc);
const hc = await GetHiddenCount();
setHiddenCount(hc);
} catch (error) {
//
}
};
void fetchCount();
}, []);
const tabs = [
{ label: 'All', value: '', count: allCount },
{ label: 'Active', value: 'active', count: 3 },
{ label: 'Pending', value: 'pending', count: 1 },
{ label: 'Blocked', value: 'blocked', count: 1 },
] as const;
const hasFilters = status || email || phone;
const hasFilters = status || email || phone || visible || name || type;
return (
<div>
<Tabs onChange={handleStatusChange} sx={{ px: 3 }} value={status ?? ''} variant="scrollable">
<Tabs
onChange={handleVisibleChange}
sx={{ px: 3 }}
value={visible ?? ''}
variant="scrollable"
//
>
{tabs.map((tab) => (
<Tab
icon={<Chip label={tab.count} size="small" variant="soft" />}
@@ -146,35 +203,37 @@ export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: Customer
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap', px: 3, py: 2 }}>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto', flexWrap: 'wrap' }}>
<FilterButton
displayValue={email}
label="Email"
displayValue={name}
label={t('Name')}
onFilterApply={(value) => {
handleEmailChange(value as string);
handleNameChange(value as string);
}}
onFilterDelete={() => {
handleEmailChange();
handleNameChange();
}}
popover={<EmailFilterPopover />}
value={email}
popover={<NameFilterPopover />}
value={name}
/>
<FilterButton
displayValue={phone}
label="Phone number"
displayValue={type}
label={t('Type')}
onFilterApply={(value) => {
handlePhoneChange(value as string);
handleTypeChange(value as string);
}}
onFilterDelete={() => {
handlePhoneChange();
handleTypeChange();
}}
popover={<PhoneFilterPopover />}
value={phone}
popover={<TypeFilterPopover />}
value={type}
/>
{hasFilters ? <Button onClick={handleClearFilters}>{t('Clear filters')}</Button> : null}
</Stack>
{selection.selectedAny ? (
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<Typography color="text.secondary" variant="body2">
{selection.selected.size} selected
{selection.selected.size} {t('selected')}
</Typography>
<Button color="error" variant="contained">
{t('Delete')}
@@ -190,7 +249,7 @@ export function LpCategoriesFilters({ filters = {}, sortDir = 'desc' }: Customer
);
}
function EmailFilterPopover(): React.JSX.Element {
function TypeFilterPopover(): React.JSX.Element {
const { t } = useTranslation();
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
@@ -200,7 +259,7 @@ function EmailFilterPopover(): React.JSX.Element {
}, [initialValue]);
return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title={t('Filter by email')}>
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title={t('Filter by type')}>
<FormControl>
<OutlinedInput
onChange={(event) => {
@@ -226,8 +285,78 @@ function EmailFilterPopover(): React.JSX.Element {
);
}
function NameFilterPopover(): React.JSX.Element {
const { t } = useTranslation();
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title={t('Filter by name')}>
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
{t('Apply')}
</Button>
</FilterPopover>
);
}
function EmailFilterPopover(): React.JSX.Element {
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title="Filter by email">
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
Apply
</Button>
</FilterPopover>
);
}
function PhoneFilterPopover(): React.JSX.Element {
const { t } = useTranslation();
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
@@ -236,7 +365,7 @@ function PhoneFilterPopover(): React.JSX.Element {
}, [initialValue]);
return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title={t('Filter by phone number')}>
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title="Filter by phone number">
<FormControl>
<OutlinedInput
onChange={(event) => {
@@ -248,7 +377,6 @@ function PhoneFilterPopover(): React.JSX.Element {
}
}}
value={value}
placeholder={t('Enter phone number')}
/>
</FormControl>
<Button
@@ -257,7 +385,7 @@ function PhoneFilterPopover(): React.JSX.Element {
}}
variant="contained"
>
{t('Apply')}
Apply
</Button>
</FilterPopover>
);

View File

@@ -7,25 +7,42 @@ function noop(): void {
return undefined;
}
interface CustomersPaginationProps {
interface LpCategoriesPaginationProps {
count: number;
page: number;
setPage: (page: number) => void;
setRowsPerPage: (page: number) => void;
rowsPerPage: number;
}
export function LpCategoriesPagination({ count, page }: CustomersPaginationProps): React.JSX.Element {
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<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value));
// console.log(parseInt(event.target.value));
};
return (
<TablePagination
component="div"
count={count}
onPageChange={noop}
onRowsPerPageChange={noop}
page={page}
rowsPerPage={5}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[5, 10, 25]}
//
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
);
}

View File

@@ -29,77 +29,6 @@ import type { ColumnDef } from '@/components/core/data-table';
import type { LessonCategory } from '../lesson_category/types';
import { useLpCategoriesSelection } from './lp-categories-selection-context';
const columns1 = [
{
formatter: (row): React.JSX.Element => (
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<Avatar src={row.avatar} />{' '}
<div>
<Link
color="inherit"
component={RouterLink}
href={paths.dashboard.customers.details('1')}
sx={{ whiteSpace: 'nowrap' }}
variant="subtitle2"
>
{row.name}
</Link>
<Typography color="text.secondary" variant="body2">
{row.email}
</Typography>
</div>
</Stack>
),
name: 'Name',
width: '250px',
},
{
formatter: (row): React.JSX.Element => (
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<LinearProgress sx={{ flex: '1 1 auto' }} value={row.quota} variant="determinate" />
<Typography color="text.secondary" variant="body2">
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)}
</Typography>
</Stack>
),
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: <CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" /> },
blocked: { label: 'Blocked', icon: <MinusIcon color="var(--mui-palette-error-main)" /> },
pending: { label: 'Pending', icon: <ClockIcon color="var(--mui-palette-warning-main)" weight="fill" /> },
} as const;
const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
return <Chip icon={icon} label={label} size="small" variant="outlined" />;
},
name: 'Status',
width: '150px',
},
{
formatter: (): React.JSX.Element => (
<IconButton component={RouterLink} href={paths.dashboard.customers.details('1')}>
<PencilSimpleIcon />
</IconButton>
),
name: 'Actions',
hideName: true,
width: '100px',
align: 'right',
},
] satisfies ColumnDef<LpCategory>[];
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCategory>[] {
return [
{
@@ -134,7 +63,9 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCateg
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<LinearProgress sx={{ flex: '1 1 auto' }} value={row.quota} variant="determinate" />
<Typography color="text.secondary" variant="body2">
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)}
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(
row?.quota ? row.quota / 100 : 0
)}
</Typography>
</Stack>
),
@@ -153,7 +84,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCateg
pending: { label: 'Pending', icon: <ClockIcon color="var(--mui-palette-warning-main)" weight="fill" /> },
NA: { label: 'NA', icon: <ClockIcon color="var(--mui-palette-warning-main)" weight="fill" /> },
} as const;
const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
const { label, icon } = mapping[row.visible] ?? { label: 'Unknown', icon: null };
return (
<Button
@@ -211,7 +142,8 @@ export interface LpCategoriesTableProps {
}
function getCatImageFromId(row: LpCategory): string | undefined {
return `http://127.0.0.1:8090/api/files/${row.collectionId}/${row.id}/${row.cat_image}`;
return 'get cat image from id';
// return `http://127.0.0.1:8090/api/files/${'row.collectionId'}/${'row.id'}/${'row.cat_image'}`;
}
export function LpCategoriesTable({ rows }: LpCategoriesTableProps): React.JSX.Element {

View File

@@ -1,3 +1,4 @@
// RULES: COL_<COLLECTION_NAME> = "<name in dbml file>"
const COL_LESSON_TYPES = 'LessonsTypes';
const COL_LESSON_CATEGORIES = 'LessonsCategories';
const NO_VALUE = 'NO_VALUE';

View File

@@ -1,14 +1,26 @@
# AI GUIDELINE
## Background information and References
## getting started
please read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml`
no need to reply me what you are going on and your digest in this phase.
just reply me "OK" when done
please read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/schema.json`
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
please look into the md files in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/_AI_GUIDELINE`
please read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
this is file in dbml syntax state the main database
please read, remember and link up the ideas, i will tell you the task afterwards
please read `<base_dir>/002_source/cms/src/db/schema.json`
this is the file of live pocketbase schema output
please look into the md files in folder `<base_dir>/002_source/cms/_AI_GUIDELINE`
please read, remember and link up the ideas in file stated above,
i will tell you the task afterwards
this is now not in debug phase,
so, no need to reply me what you are going on or your insight throught the prompt.
just reply me "OK" when done
---
@@ -45,3 +57,25 @@ when you draft coding, review file and append with `.tsx.draft`
## tasks
Thanks
---
please take a look in `schema.dbml` and `schema.json`,
associate the collection from json file to the table in dbml file
please modify the `schema.dbml` to align with `schema.json`
to the collection `QuizLPCategories` align the dbml file in the previous prompt
---
please revise
please revise
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory`
to the collection `QuizLPCategories` align the dbml file in the previous prompt

View File

@@ -0,0 +1,8 @@
import { COL_QUIZ_LISTENINGS } from '@/constants';
import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb';
export default function getQuizListeningById(id: string): Promise<RecordModel> {
return pb.collection(COL_QUIZ_LISTENINGS).getOne(id);
}

View File

@@ -0,0 +1,14 @@
// REQ0006
import { COL_QUIZ_LISTENINGS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetHiddenCount(): Promise<number> {
try {
const result = await pb.collection(COL_QUIZ_LISTENINGS).getList(1, 9999, { filter: 'visible = "hidden"' });
const { totalItems: count } = result;
return count;
} catch (error) {
return 0;
}
}

View File

@@ -0,0 +1,14 @@
// REQ0006
import { COL_QUIZ_LISTENINGS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetVisibleCount(): Promise<number> {
try {
const result = await pb.collection(COL_QUIZ_LISTENINGS).getList(1, 9999, { filter: 'visible = "visible"' });
const { totalItems: count } = result;
return count;
} catch (error) {
return 0;
}
}

View File

@@ -0,0 +1,22 @@
import { COL_QUIZ_LISTENINGS } from '@/constants';
import type { ListResult, RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb';
interface ListWithOptionParams {
currentPage: number;
rowsPerPage: number;
listOption?: {
filter?: string;
sort?: string;
expand?: string;
};
}
export default function listWithOption({
currentPage,
rowsPerPage,
listOption = {},
}: ListWithOptionParams): Promise<ListResult<RecordModel>> {
return pb.collection(COL_QUIZ_LISTENINGS).getList(currentPage + 1, rowsPerPage, listOption);
}

View File

@@ -1,13 +1,59 @@
export interface LpCategory {
cat_image?: string;
isEmpty?: boolean;
collectionId?: string;
id: string;
name: string;
avatar?: string;
email: string;
phone?: string;
quota: number;
status: 'pending' | 'active' | 'blocked';
createdAt: Date;
/*
RULES:
please follow the naming convention
*/
export interface LpCategoryFormProps {
name: string; // corresponds to cat_name
description: string; // additional business field
isActive: boolean; // additional business field
order: number; // corresponds to pos
imageUrl: string; // corresponds to cat_image
initAnswer?: any; // corresponds to init_answer
}
export interface RestLpCategoryUpdateForm {
id: string;
data: LpCategoryFormProps;
}
// RULES: this one should be obsoleted, no need to take care
// export function safeAssignment(inTemp: LpCategory | RecordModel): LpCategory {
// const { id, name, description, isActive, order, imageUrl, createdAt, updatedAt, initAnswer } = {
// ...defaultLpCategory,
// ...inTemp,
// };
// return {
// id,
// name,
// description,
// isActive,
// order,
// imageUrl,
// createdAt: dayjs(createdAt).toDate(),
// updatedAt: updatedAt ? dayjs(updatedAt).toDate() : new Date(),
// initAnswer,
// visible: 'public', // default value
// };
// }
export interface LpCategory {
isEmpty?: boolean;
id: string;
name: string; // corresponds to cat_name
description: string; // additional business field
isActive: boolean; // additional business field
order: number; // corresponds to pos
imageUrl: string; // corresponds to cat_image
createdAt: Date;
updatedAt: Date; // new field
initAnswer?: any; // corresponds to init_answer
visible: 'active' | 'blocked' | 'pending' | 'NA'; // additional business field
quota?: number;
//
email?: string;
phone?: string;
status: string;
}
export type CreateForm = LpCategoryFormProps;