1664 lines
48 KiB
XML
1664 lines
48 KiB
XML
This file is a merged representation of the entire codebase, combined into a single document by Repomix.
|
|
|
|
<file_summary>
|
|
This section contains a summary of this file.
|
|
|
|
<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.
|
|
</purpose>
|
|
|
|
<file_format>
|
|
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
|
|
</file_format>
|
|
|
|
<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.
|
|
</usage_guidelines>
|
|
|
|
<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)
|
|
</notes>
|
|
|
|
<additional_info>
|
|
|
|
</additional_info>
|
|
|
|
</file_summary>
|
|
|
|
<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
|
|
</directory_structure>
|
|
|
|
<files>
|
|
This section contains the contents of the repository's files.
|
|
|
|
<file path="categories/[cat_id]/BasicDetailCard.tsx">
|
|
'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 (
|
|
<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={model.id}
|
|
size="small"
|
|
variant="soft"
|
|
/>
|
|
),
|
|
},
|
|
{ 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 => (
|
|
<PropertyItem
|
|
key={item.key}
|
|
name={item.key}
|
|
value={item.value}
|
|
/>
|
|
)
|
|
)}
|
|
</PropertyList>
|
|
</Card>
|
|
);
|
|
}
|
|
</file>
|
|
|
|
<file path="categories/[cat_id]/page.tsx">
|
|
'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<boolean>(true);
|
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
|
|
|
//
|
|
const [showLessonCategory, setShowLessonCategory] = React.useState<MfCategory>(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 <FormLoading />;
|
|
if (showError.show)
|
|
return (
|
|
<ErrorDisplay
|
|
message={t('error.unable-to-process-request')}
|
|
code="500"
|
|
details={showError.detail}
|
|
/>
|
|
);
|
|
|
|
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.mf_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' }}
|
|
>
|
|
<TitleCard lpModel={showLessonCategory} />
|
|
</Stack>
|
|
</Stack>
|
|
<Grid
|
|
container
|
|
spacing={4}
|
|
>
|
|
<Grid
|
|
lg={4}
|
|
xs={12}
|
|
>
|
|
<Stack spacing={4}>
|
|
<BasicDetailCard
|
|
lpModel={showLessonCategory}
|
|
handleEditClick={handleEditClick}
|
|
/>
|
|
<SampleSecurityCard />
|
|
</Stack>
|
|
</Grid>
|
|
<Grid
|
|
lg={8}
|
|
xs={12}
|
|
>
|
|
<Stack spacing={4}>
|
|
<SamplePaymentCard />
|
|
<SampleAddressCard />
|
|
<Notifications notifications={SampleNotifications} />
|
|
</Stack>
|
|
</Grid>
|
|
</Grid>
|
|
</Stack>
|
|
</Box>
|
|
);
|
|
}
|
|
</file>
|
|
|
|
<file path="categories/[cat_id]/TitleCard.tsx">
|
|
'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 (
|
|
<>
|
|
<Stack
|
|
direction="row"
|
|
spacing={2}
|
|
sx={{ alignItems: 'center', flex: '1 1 auto' }}
|
|
>
|
|
<Avatar
|
|
variant="rounded"
|
|
src={getImageUrlFrRecord(lpModel)}
|
|
sx={{ '--Avatar-size': '64px' }}
|
|
>
|
|
{t('empty')}
|
|
</Avatar>
|
|
<div>
|
|
<Stack
|
|
direction="row"
|
|
spacing={2}
|
|
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
|
|
>
|
|
<Typography variant="h4">{lpModel.cat_name}</Typography>
|
|
<Chip
|
|
icon={
|
|
<CheckCircleIcon
|
|
color="var(--mui-palette-success-main)"
|
|
weight="fill"
|
|
/>
|
|
}
|
|
label={lpModel.visible}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
</Stack>
|
|
<Typography
|
|
color="text.secondary"
|
|
variant="body1"
|
|
>
|
|
{lpModel.slug}
|
|
</Typography>
|
|
</div>
|
|
</Stack>
|
|
<div>
|
|
<Button
|
|
endIcon={<CaretDownIcon />}
|
|
variant="contained"
|
|
>
|
|
{t('list.action')}
|
|
</Button>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
</file>
|
|
|
|
<file path="categories/create/page.tsx">
|
|
'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 (
|
|
<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.mf_categories.list}
|
|
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
|
variant="subtitle2"
|
|
>
|
|
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
|
{t('title')}
|
|
</Link>
|
|
</div>
|
|
<div>
|
|
<Typography variant="h4">{t('create.title')}</Typography>
|
|
</div>
|
|
</Stack>
|
|
<MfCategoryCreateForm />
|
|
</Stack>
|
|
</Box>
|
|
);
|
|
}
|
|
</file>
|
|
|
|
<file path="categories/edit/[cat_id]/_PROMPT.md">
|
|
# 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>
|
|
|
|
<file path="categories/edit/[cat_id]/page.tsx">
|
|
'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 (
|
|
<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.mf_categories.list}
|
|
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
|
variant="subtitle2"
|
|
>
|
|
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
|
{t('edit.title')}
|
|
</Link>
|
|
</div>
|
|
<div>
|
|
<Typography variant="h4">{t('edit.title')}</Typography>
|
|
</div>
|
|
</Stack>
|
|
<MfCategoryEditForm />
|
|
</Stack>
|
|
</Box>
|
|
);
|
|
}
|
|
</file>
|
|
|
|
<file path="categories/lp-categories-sample-data.tsx">
|
|
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>
|
|
|
|
<file path="categories/page.tsx">
|
|
'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<MfCategory[]>([]);
|
|
//
|
|
|
|
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
|
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
|
//
|
|
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
|
const [f, setF] = React.useState<MfCategory[]>([]);
|
|
const [currentPage, setCurrentPage] = React.useState<number>(1);
|
|
const [recordCount, setRecordCount] = React.useState<number>(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<void> => {
|
|
try {
|
|
const models: ListResult<RecordModel> = 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 <FormLoading />;
|
|
|
|
if (showError.show)
|
|
return (
|
|
<ErrorDisplay
|
|
message={t('error.unable-to-process-request')}
|
|
code={-1}
|
|
details={showError.detail}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
maxWidth: 'var(--Content-maxWidth)',
|
|
m: 'var(--Content-margin)',
|
|
p: 'var(--Content-padding)',
|
|
width: 'var(--Content-width)',
|
|
}}
|
|
>
|
|
<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('list.title')}</Typography>
|
|
</Box>
|
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
<LoadingButton
|
|
loading={isLoadingAddPage}
|
|
onClick={(): void => {
|
|
setIsLoadingAddPage(true);
|
|
router.push(paths.dashboard.mf_categories.create);
|
|
}}
|
|
startIcon={<PlusIcon />}
|
|
variant="contained"
|
|
>
|
|
{t('list.add')}
|
|
</LoadingButton>
|
|
</Box>
|
|
</Stack>
|
|
<MfCategoriesSelectionProvider lessonCategories={f}>
|
|
<Card>
|
|
<MfCategoriesFilters
|
|
filters={{ email, phone, status, name, visible, type }}
|
|
fullData={lessonCategoriesData}
|
|
sortDir={sortDir}
|
|
/>
|
|
<Divider />
|
|
<Box sx={{ overflowX: 'auto' }}>
|
|
<MfCategoriesTable
|
|
reloadRows={reloadRows}
|
|
rows={f}
|
|
/>
|
|
</Box>
|
|
<Divider />
|
|
<MfCategoriesPagination
|
|
count={recordCount}
|
|
page={currentPage}
|
|
rowsPerPage={rowsPerPage}
|
|
setPage={setCurrentPage}
|
|
setRowsPerPage={setRowsPerPage}
|
|
/>
|
|
</Card>
|
|
</MfCategoriesSelectionProvider>
|
|
</Stack>
|
|
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
|
|
<pre>{JSON.stringify(f, null, 2)}</pre>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// 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>
|
|
|
|
<file path="questions/[cat_id]/BasicDetailCard.tsx">
|
|
'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 (
|
|
<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={model.id}
|
|
size="small"
|
|
variant="soft"
|
|
/>
|
|
),
|
|
},
|
|
{ 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 => (
|
|
<PropertyItem
|
|
key={item.key}
|
|
name={item.key}
|
|
value={item.value}
|
|
/>
|
|
)
|
|
)}
|
|
</PropertyList>
|
|
</Card>
|
|
);
|
|
}
|
|
</file>
|
|
|
|
<file path="questions/[cat_id]/page.tsx">
|
|
'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<boolean>(true);
|
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
|
|
|
//
|
|
const [showLessonQuestion, setShowLessonQuestion] = React.useState<MfQuestion>(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 <FormLoading />;
|
|
if (showError.show)
|
|
return (
|
|
<ErrorDisplay
|
|
message={t('error.unable-to-process-request')}
|
|
code="500"
|
|
details={showError.detail}
|
|
/>
|
|
);
|
|
|
|
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.mf_questions.list}
|
|
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
|
variant="subtitle2"
|
|
>
|
|
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
|
{t('edit.title')}
|
|
</Link>
|
|
</div>
|
|
<Stack
|
|
direction={{ xs: 'column', sm: 'row' }}
|
|
spacing={3}
|
|
sx={{ alignItems: 'flex-start' }}
|
|
>
|
|
<TitleCard lpModel={showLessonQuestion} />
|
|
</Stack>
|
|
</Stack>
|
|
<Grid
|
|
container
|
|
spacing={4}
|
|
>
|
|
<Grid
|
|
lg={4}
|
|
xs={12}
|
|
>
|
|
<Stack spacing={4}>
|
|
<BasicDetailCard
|
|
lpModel={showLessonQuestion}
|
|
handleEditClick={handleEditClick}
|
|
/>
|
|
<SampleSecurityCard />
|
|
</Stack>
|
|
</Grid>
|
|
<Grid
|
|
lg={8}
|
|
xs={12}
|
|
>
|
|
<Stack spacing={4}>
|
|
<SamplePaymentCard />
|
|
<SampleAddressCard />
|
|
<Notifications notifications={SampleNotifications} />
|
|
</Stack>
|
|
</Grid>
|
|
</Grid>
|
|
</Stack>
|
|
</Box>
|
|
);
|
|
}
|
|
</file>
|
|
|
|
<file path="questions/[cat_id]/TitleCard.tsx">
|
|
'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 (
|
|
<>
|
|
<Stack
|
|
direction="row"
|
|
spacing={2}
|
|
sx={{ alignItems: 'center', flex: '1 1 auto' }}
|
|
>
|
|
<Avatar
|
|
variant="rounded"
|
|
src={getImageUrlFrRecord(lpModel)}
|
|
sx={{ '--Avatar-size': '64px' }}
|
|
>
|
|
{t('empty')}
|
|
</Avatar>
|
|
<div>
|
|
<Stack
|
|
direction="row"
|
|
spacing={2}
|
|
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
|
|
>
|
|
<Typography variant="h4">{lpModel.cat_name}</Typography>
|
|
<Chip
|
|
icon={
|
|
<CheckCircleIcon
|
|
color="var(--mui-palette-success-main)"
|
|
weight="fill"
|
|
/>
|
|
}
|
|
label={lpModel.visible}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
</Stack>
|
|
<Typography
|
|
color="text.secondary"
|
|
variant="body1"
|
|
>
|
|
{lpModel.slug}
|
|
</Typography>
|
|
</div>
|
|
</Stack>
|
|
<div>
|
|
<Button
|
|
endIcon={<CaretDownIcon />}
|
|
variant="contained"
|
|
>
|
|
{t('list.action')}
|
|
</Button>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
</file>
|
|
|
|
<file path="questions/create/page.tsx">
|
|
'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 (
|
|
<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.mf_questions.list}
|
|
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
|
variant="subtitle2"
|
|
>
|
|
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
|
{t('title')}
|
|
</Link>
|
|
</div>
|
|
<div>
|
|
<Typography variant="h4">{t('create.title')}</Typography>
|
|
</div>
|
|
</Stack>
|
|
<MfQuestionCreateForm />
|
|
</Stack>
|
|
</Box>
|
|
);
|
|
}
|
|
</file>
|
|
|
|
<file path="questions/edit/[cat_id]/_PROMPT.md">
|
|
# 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>
|
|
|
|
<file path="questions/edit/[cat_id]/page.tsx">
|
|
'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 (
|
|
<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.mf_questions.list}
|
|
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
|
variant="subtitle2"
|
|
>
|
|
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
|
{t('edit.title')}
|
|
</Link>
|
|
</div>
|
|
<div>
|
|
<Typography variant="h4">{t('edit.title')}</Typography>
|
|
</div>
|
|
</Stack>
|
|
<MfQuestionEditForm />
|
|
</Stack>
|
|
</Box>
|
|
);
|
|
}
|
|
</file>
|
|
|
|
<file path="questions/lp-categories-sample-data.tsx">
|
|
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>
|
|
|
|
<file path="questions/page.tsx">
|
|
'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<MfQuestion[]>([]);
|
|
//
|
|
|
|
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
|
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
|
//
|
|
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
|
const [f, setF] = React.useState<MfQuestion[]>([]);
|
|
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
|
const [recordCount, setRecordCount] = React.useState<number>(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<void> => {
|
|
try {
|
|
const models: ListResult<RecordModel> = 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 <FormLoading />;
|
|
|
|
if (showError.show)
|
|
return (
|
|
<ErrorDisplay
|
|
message={t('error.unable-to-process-request')}
|
|
code={-1}
|
|
details={showError.detail}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
maxWidth: 'var(--Content-maxWidth)',
|
|
m: 'var(--Content-margin)',
|
|
p: 'var(--Content-padding)',
|
|
width: 'var(--Content-width)',
|
|
}}
|
|
>
|
|
<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('list.title')}</Typography>
|
|
</Box>
|
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
<LoadingButton
|
|
loading={isLoadingAddPage}
|
|
onClick={(): void => {
|
|
setIsLoadingAddPage(true);
|
|
router.push(paths.dashboard.lp_questions.create);
|
|
}}
|
|
startIcon={<PlusIcon />}
|
|
variant="contained"
|
|
>
|
|
{t('list.add')}
|
|
</LoadingButton>
|
|
</Box>
|
|
</Stack>
|
|
<MfQuestionsSelectionProvider lessonQuestions={f}>
|
|
<Card>
|
|
<MfQuestionsFilters
|
|
filters={{ email, phone, status, name, visible, type }}
|
|
fullData={lessonQuestionsData}
|
|
sortDir={sortDir}
|
|
/>
|
|
<Divider />
|
|
<Box sx={{ overflowX: 'auto' }}>
|
|
<MfQuestionsTable
|
|
reloadRows={reloadRows}
|
|
rows={f}
|
|
/>
|
|
</Box>
|
|
<Divider />
|
|
<MfQuestionsPagination
|
|
count={recordCount}
|
|
page={currentPage}
|
|
rowsPerPage={rowsPerPage}
|
|
setPage={setCurrentPage}
|
|
setRowsPerPage={setRowsPerPage}
|
|
/>
|
|
</Card>
|
|
</MfQuestionsSelectionProvider>
|
|
</Stack>
|
|
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
|
|
<pre>{JSON.stringify(f, null, 2)}</pre>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// 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>
|
|
|
|
</files>
|