update page,
This commit is contained in:
9
002_source/cms/src/app/_helloworld/_PROMPT.MD
Normal file
9
002_source/cms/src/app/_helloworld/_PROMPT.MD
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# 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`
|
||||||
|
|
||||||
|
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/error/index.tsx`
|
||||||
|
|
||||||
|
please draft a tsx for showing error to user thanks,
|
55
002_source/cms/src/app/_helloworld/page.tsx
Normal file
55
002_source/cms/src/app/_helloworld/page.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Typography } from '@mui/material';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import ErrorDisplay from '@/components/dashboard/error';
|
||||||
|
import FormLoading from '@/components/loading';
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
hello: {
|
||||||
|
world: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Page({ hello }: PageProps): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [state, setState] = React.useState<string>(hello.world);
|
||||||
|
|
||||||
|
//
|
||||||
|
const [showError, setShowError] = React.useState<boolean>(false);
|
||||||
|
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setShowLoading(false);
|
||||||
|
setShowError(false);
|
||||||
|
setState('blablabla');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (showLoading) return <FormLoading />;
|
||||||
|
|
||||||
|
if (showError)
|
||||||
|
return (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t('"Unable to process request"')}
|
||||||
|
code="500"
|
||||||
|
details={t('Detailed error information...')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxWidth: 'var(--Content-maxWidth)',
|
||||||
|
m: 'var(--Content-margin)',
|
||||||
|
p: 'var(--Content-padding)',
|
||||||
|
width: 'var(--Content-width)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h1">Hello World</Typography>
|
||||||
|
{state}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@@ -17,6 +17,7 @@ import { paths } from '@/paths';
|
|||||||
import { logger } from '@/lib/default-logger';
|
import { logger } from '@/lib/default-logger';
|
||||||
import { pb } from '@/lib/pb';
|
import { pb } from '@/lib/pb';
|
||||||
import { toast } from '@/components/core/toaster';
|
import { toast } from '@/components/core/toaster';
|
||||||
|
import ErrorDisplay from '@/components/dashboard/error';
|
||||||
import { defaultLessonCategory, type LessonCategory } from '@/components/dashboard/lesson_category/interfaces';
|
import { defaultLessonCategory, type LessonCategory } from '@/components/dashboard/lesson_category/interfaces';
|
||||||
import { LessonCategoriesFilters } from '@/components/dashboard/lesson_category/lesson-categories-filters';
|
import { LessonCategoriesFilters } from '@/components/dashboard/lesson_category/lesson-categories-filters';
|
||||||
import type { Filters } from '@/components/dashboard/lesson_category/lesson-categories-filters';
|
import type { Filters } from '@/components/dashboard/lesson_category/lesson-categories-filters';
|
||||||
@@ -48,6 +49,9 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||||
const [currentPage, setCurrentPage] = React.useState<number>(1);
|
const [currentPage, setCurrentPage] = React.useState<number>(1);
|
||||||
|
//
|
||||||
|
const [showError, setShowError] = React.useState<boolean>(false);
|
||||||
|
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||||
|
|
||||||
//
|
//
|
||||||
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
||||||
@@ -57,6 +61,8 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status: spStatus });
|
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status: spStatus });
|
||||||
|
|
||||||
const reloadRows = () => {
|
const reloadRows = () => {
|
||||||
|
setShowLoading(true);
|
||||||
|
|
||||||
pb.collection(COL_LESSON_CATEGORIES)
|
pb.collection(COL_LESSON_CATEGORIES)
|
||||||
.getList(currentPage, rowsPerPage, {})
|
.getList(currentPage, rowsPerPage, {})
|
||||||
.then((lessonCategories: ListResult<RecordModel>) => {
|
.then((lessonCategories: ListResult<RecordModel>) => {
|
||||||
@@ -72,6 +78,10 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
toast(t('dashboard.lessonTypes.list.error'));
|
toast(t('dashboard.lessonTypes.list.error'));
|
||||||
|
setShowError(true);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setShowLoading(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,7 +89,16 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
reloadRows();
|
reloadRows();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (lessonCategoriesData.length < 1) return <FormLoading />;
|
if (showLoading) return <FormLoading />;
|
||||||
|
|
||||||
|
if (showError)
|
||||||
|
return (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t('"Unable to process request"')}
|
||||||
|
code="500"
|
||||||
|
details={t('Detailed error information...')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
// return <pre>{JSON.stringify(lessonCategoriesData, null, 2)}</pre>;
|
// return <pre>{JSON.stringify(lessonCategoriesData, null, 2)}</pre>;
|
||||||
|
|
||||||
|
50
002_source/cms/src/components/_helloworld/index.tsx
Normal file
50
002_source/cms/src/components/_helloworld/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Typography } from '@mui/material';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import { Box } from '@mui/system';
|
||||||
|
import { Trash as TrashIcon } from '@phosphor-icons/react/dist/ssr/Trash';
|
||||||
|
|
||||||
|
interface PropsHelloworld {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RULES: Sample of function
|
||||||
|
function funcHelloworld(hello: string): string {
|
||||||
|
const helloworld: PropsHelloworld = { message: hello };
|
||||||
|
const output = `${helloworld.message} world!`;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RULES: sample of inner component
|
||||||
|
function InnerComponent(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography sx={{ color: 'red' }}>inner component</Typography>
|
||||||
|
<ListItemIcon />
|
||||||
|
<TrashIcon />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// RULES: naming should be in Pascal case
|
||||||
|
// RULES: sample of main component
|
||||||
|
function MainComponent(): React.JSX.Element {
|
||||||
|
const [state, setState] = React.useState<string>('');
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setState(funcHelloworld('hello'));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// you should obey react/jsx-no-useless-fragment
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{state} <InnerComponent />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// RULES: component should be exported
|
||||||
|
export default MainComponent;
|
9
002_source/cms/src/components/dashboard/error/_PROMPT.MD
Normal file
9
002_source/cms/src/components/dashboard/error/_PROMPT.MD
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# task
|
||||||
|
|
||||||
|
## instruction
|
||||||
|
|
||||||
|
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/_helloworld/index.tsx`
|
||||||
|
|
||||||
|
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/error/index.tsx`
|
||||||
|
|
||||||
|
please draft a tsx for showing error to user thanks,
|
84
002_source/cms/src/components/dashboard/error/index.tsx
Normal file
84
002_source/cms/src/components/dashboard/error/index.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Alert, AlertTitle, Collapse, Typography } from '@mui/material';
|
||||||
|
import { Box } from '@mui/system';
|
||||||
|
import { Warning as WarningIcon } from '@phosphor-icons/react/dist/ssr/Warning';
|
||||||
|
|
||||||
|
interface PropsError {
|
||||||
|
message: string;
|
||||||
|
code?: string | number;
|
||||||
|
details?: string;
|
||||||
|
severity?: 'error' | 'warning' | 'info' | 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
// RULES: Sample of function
|
||||||
|
function formatErrorMessage(message: string, code?: string | number): string {
|
||||||
|
return code ? `[${code}] ${message}` : message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RULES: sample of inner component
|
||||||
|
function ErrorDetails({ details }: { details: string }): React.JSX.Element {
|
||||||
|
const [expanded, setExpanded] = React.useState<boolean>(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
textDecoration: 'underline',
|
||||||
|
mt: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setExpanded(!expanded);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{expanded ? 'Hide Details' : 'Show Details'}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Collapse in={expanded}>
|
||||||
|
<Box sx={{ mt: 1, p: 1, bgcolor: 'background.paper' }}>
|
||||||
|
<Typography variant="body2" component="pre" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{details}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// RULES: naming should be in Pascal case
|
||||||
|
// RULES: sample of main component
|
||||||
|
function ErrorDisplay({ message, code, details, severity = 'error' }: PropsError): React.JSX.Element {
|
||||||
|
const [formattedMessage, setFormattedMessage] = React.useState<string>('');
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setFormattedMessage(formatErrorMessage(message, code));
|
||||||
|
}, [message, code]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 2, maxWidth: 800, width: '100%' }}>
|
||||||
|
<Alert
|
||||||
|
severity={severity}
|
||||||
|
icon={<WarningIcon weight="fill" />}
|
||||||
|
sx={{
|
||||||
|
'& .MuiAlert-message': {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertTitle>{code ? `Error ${code}` : 'Error'}</AlertTitle>
|
||||||
|
|
||||||
|
<Typography variant="body1" gutterBottom>
|
||||||
|
{formattedMessage}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{details && <ErrorDetails details={details} />}
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// RULES: component should be exported
|
||||||
|
export default ErrorDisplay;
|
@@ -33,6 +33,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
formatter: (row): React.JSX.Element => (
|
formatter: (row): React.JSX.Element => (
|
||||||
|
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||||
<Link
|
<Link
|
||||||
color="inherit"
|
color="inherit"
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
@@ -52,6 +53,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
|
|||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Link>
|
</Link>
|
||||||
|
</Stack>
|
||||||
),
|
),
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
width: '200px',
|
width: '200px',
|
||||||
@@ -74,6 +76,9 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
|
|||||||
|
|
||||||
{
|
{
|
||||||
formatter: (row): React.JSX.Element => {
|
formatter: (row): React.JSX.Element => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const mapping = {
|
const mapping = {
|
||||||
active: { label: 'Active', icon: <CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" /> },
|
active: { label: 'Active', icon: <CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" /> },
|
||||||
blocked: { label: 'Blocked', icon: <MinusIcon color="var(--mui-palette-error-main)" /> },
|
blocked: { label: 'Blocked', icon: <MinusIcon color="var(--mui-palette-error-main)" /> },
|
||||||
@@ -82,7 +87,17 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
|
|||||||
} as const;
|
} as const;
|
||||||
const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
|
const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
|
||||||
|
|
||||||
return <Chip icon={icon} label={label} size="small" variant="outlined" />;
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
toast.error('sorry but not implementd');
|
||||||
|
}}
|
||||||
|
style={{ backgroundColor: 'transparent' }}
|
||||||
|
>
|
||||||
|
{/* <Chip icon={icon} label={label} size="small" variant="outlined" /> */}
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
name: 'Status',
|
name: 'Status',
|
||||||
width: '150px',
|
width: '150px',
|
||||||
@@ -95,10 +110,25 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
|
|||||||
width: '100px',
|
width: '100px',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter: (): React.JSX.Element => (
|
formatter: (row): React.JSX.Element => (
|
||||||
<IconButton component={RouterLink} href={paths.dashboard.lesson_categories.details('1')}>
|
<Stack direction="row" spacing={1}>
|
||||||
|
<IconButton
|
||||||
|
//
|
||||||
|
component={RouterLink}
|
||||||
|
href={paths.dashboard.lesson_categories.details(row.id)}
|
||||||
|
>
|
||||||
<PencilSimpleIcon />
|
<PencilSimpleIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
color="error"
|
||||||
|
disabled={row.isEmpty}
|
||||||
|
onClick={() => {
|
||||||
|
handleDeleteClick(row.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashSimpleIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
),
|
),
|
||||||
name: 'Actions',
|
name: 'Actions',
|
||||||
hideName: true,
|
hideName: true,
|
||||||
|
@@ -33,7 +33,6 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonT
|
|||||||
{
|
{
|
||||||
formatter: (row): React.JSX.Element => (
|
formatter: (row): React.JSX.Element => (
|
||||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||||
<div>
|
|
||||||
<Link
|
<Link
|
||||||
color={row.isEmpty ? 'disabled' : 'inherit'}
|
color={row.isEmpty ? 'disabled' : 'inherit'}
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
@@ -43,7 +42,6 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonT
|
|||||||
>
|
>
|
||||||
{row.isEmpty ? '--' : row.name}
|
{row.isEmpty ? '--' : row.name}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
),
|
),
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
@@ -129,16 +127,21 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonT
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter(row) {
|
formatter(row) {
|
||||||
return row.isEmpty ? '--' : dayjs(row.createdAt).format('MMM D, YYYY h:mm A');
|
return row.isEmpty ? '--' : dayjs(row.createdAt).format('MMM D, YYYY ');
|
||||||
},
|
},
|
||||||
name: 'Created at',
|
name: 'Created at',
|
||||||
width: '200px',
|
width: '100px',
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
formatter: (row): React.JSX.Element => (
|
formatter: (row): React.JSX.Element => (
|
||||||
<Stack direction="row" spacing={1}>
|
<Stack direction="row" spacing={1}>
|
||||||
<IconButton disabled={row.isEmpty} component={RouterLink} href={paths.dashboard.lesson_types.details(row.id)}>
|
<IconButton
|
||||||
|
//
|
||||||
|
disabled={row.isEmpty}
|
||||||
|
component={RouterLink}
|
||||||
|
href={paths.dashboard.lesson_types.details(row.id)}
|
||||||
|
>
|
||||||
<PencilSimpleIcon />
|
<PencilSimpleIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -153,6 +156,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonT
|
|||||||
</Stack>
|
</Stack>
|
||||||
),
|
),
|
||||||
name: 'Actions',
|
name: 'Actions',
|
||||||
|
hideName: true,
|
||||||
width: '100px',
|
width: '100px',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user