update build ok,

This commit is contained in:
louiscklaw
2025-04-16 10:55:48 +08:00
parent 9b39e82488
commit b4fed69da6
9 changed files with 197 additions and 45 deletions

View File

@@ -2,6 +2,7 @@
import * as React from 'react'; import * as React from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { COL_LESSON_TYPES } from '@/constants';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
@@ -9,7 +10,7 @@ import Divider from '@mui/material/Divider';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
import type { RecordModel } from 'pocketbase'; import type { ListResult, RecordModel } from 'pocketbase';
import PocketBase from 'pocketbase'; import PocketBase from 'pocketbase';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -17,7 +18,7 @@ import { paths } from '@/paths';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType'; import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType';
import { safeAssignment } from '@/components/dashboard/lesson_type/interfaces'; import { emptyLessonType, safeAssignment } from '@/components/dashboard/lesson_type/interfaces';
import { LessonTypesFilters } from '@/components/dashboard/lesson_type/lesson-types-filters'; import { LessonTypesFilters } from '@/components/dashboard/lesson_type/lesson-types-filters';
import type { Filters } from '@/components/dashboard/lesson_type/lesson-types-filters'; import type { Filters } from '@/components/dashboard/lesson_type/lesson-types-filters';
import { LessonTypesPagination } from '@/components/dashboard/lesson_type/lesson-types-pagination'; import { LessonTypesPagination } from '@/components/dashboard/lesson_type/lesson-types-pagination';
@@ -25,8 +26,6 @@ import { LessonTypesSelectionProvider } from '@/components/dashboard/lesson_type
import { LessonTypesTable } from '@/components/dashboard/lesson_type/lesson-types-table'; import { LessonTypesTable } from '@/components/dashboard/lesson_type/lesson-types-table';
import FormLoading from '@/components/loading'; import FormLoading from '@/components/loading';
import { Helloworld } from './db';
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL); const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL);
interface PageProps { interface PageProps {
@@ -47,11 +46,16 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { email, phone, sortDir, spStatus, spName, spVisible, spType } = searchParams; const { email, phone, sortDir, spStatus, spName, spVisible, spType } = searchParams;
const router = useRouter(); const router = useRouter();
const [recordCount, setRecordCount] = React.useState<number>(0);
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
const [currentPage, setCurrentPage] = React.useState<number>(1);
//
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false); const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
const [lessonTypesData, setLessonTypesData] = React.useState<LessonType[]>([]); const [lessonTypesData, setLessonTypesData] = React.useState<LessonType[]>([]);
// const [recordModel, setRecordModel] = React.useState<RecordModel[]>([]); // const [recordModel, setRecordModel] = React.useState<RecordModel[]>([]);
const sortedLessonTypes = applySort(lessonTypesData, sortDir); const sortedLessonTypes = applySort(lessonTypesData, sortDir);
const filteredLessonTypes = applyFilters(sortedLessonTypes, { const filteredLessonTypesOld = applyFilters(sortedLessonTypes, {
email, email,
phone, phone,
spStatus, spStatus,
@@ -61,19 +65,43 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
// //
}); });
const needToFill = 5 - filteredLessonTypesOld.length;
const filteredLessonTypes: LessonType[] = filteredLessonTypesOld;
for (let i = 0; i < needToFill; i++) {
filteredLessonTypes.push(emptyLessonType);
}
const reloadRows = () => { const reloadRows = () => {
pb.collection('LessonsTypes') pb.collection(COL_LESSON_TYPES)
.getFullList() .getList(currentPage, rowsPerPage, {})
.then((lessonTypes: RecordModel[]) => { .then((lessonTypes: ListResult<RecordModel>) => {
const tempLessonTypes: LessonType[] = lessonTypes.map((lt) => { // console.log(lessonTypes);
const { items, page, perPage, totalItems, totalPages } = lessonTypes;
const tempLessonTypes: LessonType[] = items.map((lt) => {
return safeAssignment(lt); return safeAssignment(lt);
}); });
setLessonTypesData(tempLessonTypes); setLessonTypesData(tempLessonTypes);
setRecordCount(totalItems);
}) })
.catch((err) => { .catch((err) => {
logger.error(err); logger.error(err);
toast(t('dashboard.lessonTypes.list.error')); toast(t('dashboard.lessonTypes.list.error'));
}); });
// pb.collection(COL_LESSON_TYPES)
// .getFullList()
// .then((lessonTypes: RecordModel[]) => {
// const tempLessonTypes: LessonType[] = lessonTypes.map((lt) => {
// return safeAssignment(lt);
// });
// setLessonTypesData(tempLessonTypes);
// })
// .catch((err) => {
// logger.error(err);
// toast(t('dashboard.lessonTypes.list.error'));
// });
}; };
React.useEffect(() => { React.useEffect(() => {
@@ -123,7 +151,13 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
<LessonTypesTable reloadRows={reloadRows} rows={filteredLessonTypes} /> <LessonTypesTable reloadRows={reloadRows} rows={filteredLessonTypes} />
</Box> </Box>
<Divider /> <Divider />
<LessonTypesPagination count={filteredLessonTypes.length + 100} page={0} /> <LessonTypesPagination
count={recordCount}
page={0}
rowsPerPage={rowsPerPage}
setRowsPerPage={setRowsPerPage}
setCurrentPage={setCurrentPage}
/>
</Card> </Card>
</LessonTypesSelectionProvider> </LessonTypesSelectionProvider>
</Stack> </Stack>

View File

@@ -4,6 +4,7 @@ import { dayjs } from '@/lib/dayjs';
export interface LessonType { export interface LessonType {
id: string; id: string;
isEmpty?: boolean;
name: string; name: string;
type: string; type: string;
pos: number; pos: number;

View File

@@ -1,6 +1,8 @@
'use client'; 'use client';
import * as React from 'react'; import * as React from 'react';
import { useRouter } from 'next/navigation';
import { COL_LESSON_TYPES } from '@/constants';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { Button, Container, Modal, Paper } from '@mui/material'; import { Button, Container, Modal, Paper } from '@mui/material';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
@@ -8,9 +10,13 @@ import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note'; import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note';
import PocketBase from 'pocketbase';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
// import { deleteLessonType } from './http-actions'; import { logger } from '@/lib/default-logger';
import { toast } from '@/components/core/toaster';
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL);
export default function ConfirmDeleteModal({ export default function ConfirmDeleteModal({
open, open,
@@ -38,23 +44,36 @@ export default function ConfirmDeleteModal({
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
}; };
const handleUserConfirmDelete = (): void => { function performDelete(id: string): Promise<void> {
return pb
.collection(COL_LESSON_TYPES)
.delete(id)
.then(() => {
toast(t('dashboard.lessonTypes.delete.success'));
reloadRows();
})
.catch((err) => {
logger.error(err);
toast(t('dashboard.lessonTypes.delete.error'));
})
.finally(() => {});
}
function handleUserConfirmDelete(): void {
if (idToDelete) { if (idToDelete) {
setIsDeleteing(true); setIsDeleteing(true);
// deleteLessonType(idToDelete) performDelete(idToDelete)
// .then(() => { .then(() => {
// reloadRows(); handleClose();
// handleClose(); setIsDeleteing(false);
// }) })
// .catch((err) => { .catch((err) => {
// // console.error(err); // console.error(err)
// setIsDeleteing(false); logger.error(err);
// }) toast(t('dashboard.lessonTypes.delete.error'));
// .finally(() => { });
// setIsDeleteing(false);
// });
} }
}; }
return ( return (
<div> <div>

View File

@@ -1,3 +1,4 @@
import { NO_NUM, NO_VALUE } from '@/constants';
import type { RecordModel } from 'pocketbase'; import type { RecordModel } from 'pocketbase';
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
@@ -50,6 +51,11 @@ export const defaultLessonType: LessonType = {
// createdAt: Date; // createdAt: Date;
}; };
export const emptyLessonType: LessonType = {
...defaultLessonType,
isEmpty: true,
};
export function safeAssignment(inTemp: LessonType | RecordModel): LessonType { export function safeAssignment(inTemp: LessonType | RecordModel): LessonType {
const { id, name, type, pos, visible, createdAt, email, quota, status } = { ...defaultLessonType, ...inTemp }; const { id, name, type, pos, visible, createdAt, email, quota, status } = { ...defaultLessonType, ...inTemp };
const oCreatedAt = dayjs(createdAt).toDate(); const oCreatedAt = dayjs(createdAt).toDate();

View File

@@ -3,6 +3,7 @@
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { COL_LESSON_TYPES } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { MenuItem } from '@mui/material'; import { MenuItem } from '@mui/material';
@@ -23,6 +24,8 @@ import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2'; import Grid from '@mui/material/Unstable_Grid2';
import type { RecordModel } from 'pocketbase';
import PocketBase from 'pocketbase';
// import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; // import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
// import axios from 'axios'; // import axios from 'axios';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
@@ -33,6 +36,10 @@ import { paths } from '@/paths';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
// import { Option } from '@/components/core/option'; // import { Option } from '@/components/core/option';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import { defaultLessonType, LessonTypeCreateForm, LessonTypeCreateFormDefault } from './interfaces';
import type { LessonType } from './ILessonType';
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL);
// import { createLessonType } from './http-actions'; // import { createLessonType } from './http-actions';
// import { LessonTypeCreateForm, LessonTypeCreateFormDefault } from './interfaces'; // import { LessonTypeCreateForm, LessonTypeCreateFormDefault } from './interfaces';
@@ -55,7 +62,7 @@ import { toast } from '@/components/core/toaster';
const schema = zod.object({ const schema = zod.object({
name: zod.string().min(1, 'Name is required').max(255), name: zod.string().min(1, 'Name is required').max(255),
type: zod.string().min(1, 'Name is required').max(255), type: zod.string().min(1, 'Name is required').max(255),
pos: zod.string().min(1, 'Phone is required').max(15), pos: zod.number().min(1, 'Phone is required').max(15),
visible_to_user: zod.string().max(255), visible_to_user: zod.string().max(255),
}); });
@@ -64,7 +71,7 @@ type Values = zod.infer<typeof schema>;
const defaultValues = { const defaultValues = {
name: '', name: '',
type: '', type: '',
pos: '1', pos: 1,
visible_to_user: 'visible', visible_to_user: 'visible',
} satisfies Values; } satisfies Values;
@@ -84,6 +91,21 @@ export function LessonTypeCreateForm(): React.JSX.Element {
const onSubmit = React.useCallback( const onSubmit = React.useCallback(
async (values: Values): Promise<void> => { async (values: Values): Promise<void> => {
setIsCreating(true); setIsCreating(true);
const data: LessonTypeCreateForm = {...LessonTypeCreateFormDefault, ...values}
pb.collection(COL_LESSON_TYPES).create(data).then((res)=>{
console.log(res)
logger.debug(res);
toast.success(t('dashboard.lessonTypes.update.success'));
setIsCreating(false);
router.push(paths.dashboard.lesson_types.list);
}).catch((err) => {
logger.error(err);
toast.error('Something went wrong!');
setIsCreating(false);})
// const tempCreate: LessonTypeCreateForm = LessonTypeCreateFormDefault; // const tempCreate: LessonTypeCreateForm = LessonTypeCreateFormDefault;
// tempCreate.name = values.name; // tempCreate.name = values.name;
// tempCreate.type = values.type; // tempCreate.type = values.type;
@@ -136,7 +158,7 @@ export function LessonTypeCreateForm(): React.JSX.Element {
display: 'inline-flex', display: 'inline-flex',
p: '4px', p: '4px',
}} }}
></Box> />
<Stack spacing={1} sx={{ alignItems: 'flex-start' }}> <Stack spacing={1} sx={{ alignItems: 'flex-start' }}>
<Typography variant="subtitle1">{t('dashboard.lessonTypes.create.avatar')}</Typography> <Typography variant="subtitle1">{t('dashboard.lessonTypes.create.avatar')}</Typography>
<Typography variant="caption">{t('dashboard.lessonTypes.create.avatarRequirements')}</Typography> <Typography variant="caption">{t('dashboard.lessonTypes.create.avatarRequirements')}</Typography>
@@ -186,7 +208,7 @@ export function LessonTypeCreateForm(): React.JSX.Element {
render={({ field }) => ( render={({ field }) => (
<FormControl error={Boolean(errors.pos)} fullWidth> <FormControl error={Boolean(errors.pos)} fullWidth>
<InputLabel required>{t('dashboard.lessonTypes.create.position')}</InputLabel> <InputLabel required>{t('dashboard.lessonTypes.create.position')}</InputLabel>
<OutlinedInput {...field} /> <OutlinedInput {...field} onChange={e => {field.onChange(parseInt(e.target.value))} } type="number"/>
{errors.pos ? <FormHelperText>{errors.pos.message}</FormHelperText> : null} {errors.pos ? <FormHelperText>{errors.pos.message}</FormHelperText> : null}
</FormControl> </FormControl>
)} )}

View File

@@ -26,8 +26,6 @@ import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2'; import Grid from '@mui/material/Unstable_Grid2';
import type { RecordModel } from 'pocketbase'; import type { RecordModel } from 'pocketbase';
import PocketBase from 'pocketbase'; import PocketBase from 'pocketbase';
// import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
// import axios from 'axios';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod'; import { z as zod } from 'zod';

View File

@@ -10,21 +10,48 @@ function noop(): void {
interface LessonTypesPaginationProps { interface LessonTypesPaginationProps {
count: number; count: number;
page: number; page: number;
rowsPerPage: number;
setRowsPerPage: React.Dispatch<React.SetStateAction<number>>;
setCurrentPage: React.Dispatch<React.SetStateAction<number>>;
} }
export function LessonTypesPagination({ count, page }: LessonTypesPaginationProps): React.JSX.Element { export function LessonTypesPagination({
count,
page,
rowsPerPage,
setRowsPerPage,
setCurrentPage,
}: LessonTypesPaginationProps): React.JSX.Element {
// You should implement the pagination using a similar logic as the filters. // You should implement the pagination using a similar logic as the filters.
// Note that when page change, you should keep the filter search params. // Note that when page change, you should keep the filter search params.
function handleRowsPerPageChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void {
console.log(e.target.value);
console.log(parseInt(e.target.value));
setRowsPerPage(parseInt(e.target.value));
}
function handlePageChange(e: React.MouseEvent<HTMLButtonElement> | null): void {
// console.log(e.target.value);
// console.log(parseInt(e.target.value));
// setCurrentPage(parseInt(e.target.value));
}
return ( return (
<TablePagination <TablePagination
component="div" component="div"
count={count} count={count}
onPageChange={noop}
onRowsPerPageChange={noop}
page={page} page={page}
rowsPerPage={5} //
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[5, 10, 25]} rowsPerPageOptions={[5, 10, 25]}
//
onPageChange={(e) => {
handlePageChange(e);
}}
onRowsPerPageChange={(e) => {
handleRowsPerPageChange(e);
}}
/> />
); );
} }

View File

@@ -2,6 +2,7 @@
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { NO_NUM, NO_VALUE } from '@/constants';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Chip from '@mui/material/Chip'; import Chip from '@mui/material/Chip';
@@ -34,22 +35,58 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonT
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}> <Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<div> <div>
<Link <Link
color="inherit" color={row.isEmpty ? 'disabled' : 'inherit'}
component={RouterLink} component={RouterLink}
href={paths.dashboard.lesson_types.details(row.id)} href={paths.dashboard.lesson_types.details(row.id)}
sx={{ whiteSpace: 'nowrap' }} sx={{ whiteSpace: 'nowrap' }}
variant="subtitle2" variant="subtitle2"
> >
{row.name} {row.isEmpty ? '--' : row.name}
</Link> </Link>
</div> </div>
</Stack> </Stack>
), ),
name: 'Name', name: 'Name',
width: '250px', width: '150px',
},
{
formatter: (row): React.JSX.Element => (
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<div>
<Link
color={row.isEmpty ? 'disabled' : 'inherit'}
component={RouterLink}
href={paths.dashboard.lesson_types.details(row.id)}
sx={{ whiteSpace: 'nowrap' }}
variant="subtitle2"
>
{row.isEmpty ? '--' : row.type}
</Link>
</div>
</Stack>
),
name: 'Lesson type',
width: '150px',
},
{
formatter: (row): React.JSX.Element => (
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<div>
<Link
color={row.isEmpty ? 'disabled' : 'inherit'}
component={RouterLink}
href={paths.dashboard.lesson_types.details(row.id)}
sx={{ whiteSpace: 'nowrap' }}
variant="subtitle2"
>
{row.isEmpty ? '--' : row.pos}
</Link>
</div>
</Stack>
),
name: 'Lesson position',
width: '150px',
}, },
{ field: 'type', name: 'Lesson type', width: '150px' },
{ field: 'pos', name: 'Lesson position', width: '150px' },
{ {
formatter: (row): React.JSX.Element => { formatter: (row): React.JSX.Element => {
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
@@ -67,9 +104,13 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonT
label: t('hidden'), label: t('hidden'),
icon: <ClockIcon color="var(--mui-palette-warning-main)" weight="fill" />, icon: <ClockIcon color="var(--mui-palette-warning-main)" weight="fill" />,
}, },
no_value: {
label: t('no_value'),
icon: <ClockIcon color="var(--mui-palette-warning-main)" weight="fill" />,
},
} as const; } as const;
const { label, icon } = mapping[row.visible] ?? { label: 'Unknown', icon: null }; const { label, icon } = row.isEmpty ? { label: '--', icon: null } : mapping[row.visible];
return ( return (
<Button <Button
@@ -78,7 +119,8 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonT
}} }}
style={{ backgroundColor: 'transparent' }} style={{ backgroundColor: 'transparent' }}
> >
<Chip icon={icon} label={label} size="small" variant="outlined" /> {/* <Chip icon={icon} label={label} size="small" variant="outlined" /> */}
{label}
</Button> </Button>
); );
}, },
@@ -87,7 +129,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonT
}, },
{ {
formatter(row) { formatter(row) {
return dayjs(row.createdAt).format('MMM D, YYYY h:mm A'); return row.isEmpty ? '--' : dayjs(row.createdAt).format('MMM D, YYYY h:mm A');
}, },
name: 'Created at', name: 'Created at',
width: '200px', width: '200px',
@@ -96,11 +138,12 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonT
{ {
formatter: (row): React.JSX.Element => ( formatter: (row): React.JSX.Element => (
<Stack direction="row" spacing={1}> <Stack direction="row" spacing={1}>
<IconButton 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
color="error" color="error"
disabled={row.isEmpty}
onClick={() => { onClick={() => {
handleDeleteClick(row.id); handleDeleteClick(row.id);
}} }}

View File

@@ -1,3 +1,5 @@
const COL_LESSON_TYPES = 'LessonsTypes'; const COL_LESSON_TYPES = 'LessonsTypes';
const NO_VALUE = 'NO_VALUE';
const NO_NUM = -Infinity;
export { COL_LESSON_TYPES }; export { COL_LESSON_TYPES, NO_VALUE, NO_NUM };