update build ok,

This commit is contained in:
louiscklaw
2025-04-17 05:00:28 +08:00
parent 0648bf5bfb
commit bca5a951c8
8 changed files with 261 additions and 114 deletions

View File

@@ -15,6 +15,7 @@
"lint:fix": "next lint --fix", "lint:fix": "next lint --fix",
"lint:w": "pnpx nodemon --ext ts,tsx,json,mjs,js,jsx --delay 2 --exec \"pnpm run lint\"", "lint:w": "pnpx nodemon --ext ts,tsx,json,mjs,js,jsx --delay 2 --exec \"pnpm run lint\"",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"typecheck:w": "pnpx nodemon --ext ts,tsx,json,mjs,js,jsx --delay 2 --exec \"pnpm run typecheck\"",
"format:write": "prettier --write \"**/*.{js,jsx,mjs,ts,tsx,mdx}\" --cache", "format:write": "prettier --write \"**/*.{js,jsx,mjs,ts,tsx,mdx}\" --cache",
"format:check": "prettier --check \"**/*.{js,jsx,mjs,ts,tsx,mdx}\" --cache", "format:check": "prettier --check \"**/*.{js,jsx,mjs,ts,tsx,mdx}\" --cache",
"format:fix": "prettier --write \"**/*.{js,jsx,mjs,ts,tsx,mdx}\" --cache", "format:fix": "prettier --write \"**/*.{js,jsx,mjs,ts,tsx,mdx}\" --cache",

View File

@@ -186,5 +186,7 @@
"dashboard.lessonCategorys.edit.visible": "顯示", "dashboard.lessonCategorys.edit.visible": "顯示",
"dashboard.lessonCategorys.edit.hidden": "隱藏", "dashboard.lessonCategorys.edit.hidden": "隱藏",
"dashboard.lessonCategorys.edit.cancelButton": "取消", "dashboard.lessonCategorys.edit.cancelButton": "取消",
"dashboard.lessonCategorys.edit.updateButton": "更新" "dashboard.lessonCategorys.edit.updateButton": "更新",
"dashboard.lessonCategorys.list.title": "課堂種類",
"word-count": "字數"
} }

View File

@@ -26,6 +26,7 @@ import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning'; import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
import { useTranslation } from 'react-i18next';
import { config } from '@/config'; import { config } from '@/config';
import { paths } from '@/paths'; import { paths } from '@/paths';
@@ -40,6 +41,8 @@ import { ShippingAddress } from '@/components/dashboard/lesson_category/shipping
// export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; // export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
export default function Page(): React.JSX.Element { export default function Page(): React.JSX.Element {
let { t } = useTranslation();
return ( return (
<Box <Box
sx={{ sx={{
@@ -60,7 +63,7 @@ export default function Page(): React.JSX.Element {
variant="subtitle2" variant="subtitle2"
> >
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" /> <ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Lesson Categories {t('dashboard.lessonCategorys.list.title')}
</Link> </Link>
</div> </div>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}> <Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>

View File

@@ -17,11 +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 { import { defaultLessonCategory, type LessonCategory } from '@/components/dashboard/lesson_category/interfaces';
DBLessonCategory,
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';
import { LessonCategoriesPagination } from '@/components/dashboard/lesson_category/lesson-categories-pagination'; import { LessonCategoriesPagination } from '@/components/dashboard/lesson_category/lesson-categories-pagination';
@@ -52,18 +48,21 @@ 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 sortedLessonCategories = applySort(lessonCategoriesSampleData, sortDir);
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status: spStatus });
// //
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false); const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<LessonCategory[]>([]); const [lessonCategoriesData, setLessonCategoriesData] = React.useState<LessonCategory[]>([]);
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status: spStatus });
const reloadRows = () => { const reloadRows = () => {
pb.collection(COL_LESSON_CATEGORIES) pb.collection(COL_LESSON_CATEGORIES)
.getList(currentPage, rowsPerPage, {}) .getList(currentPage, rowsPerPage, {})
.then((lessonCategories: ListResult<RecordModel>) => { .then((lessonCategories: ListResult<RecordModel>) => {
// console.log(lessonTypes); // console.log(lessonTypes);
const { items, page, perPage, totalItems, totalPages } = lessonCategories; const { items, page, perPage, totalItems, totalPages } = lessonCategories;
const tempLessonCategories: DBLessonCategory[] = items.map((item) => { const tempLessonCategories: LessonCategory[] = items.map((item) => {
return { ...defaultLessonCategory, ...item }; return { ...defaultLessonCategory, ...item };
}); });
@@ -82,6 +81,8 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
if (lessonCategoriesData.length < 1) return <FormLoading />; if (lessonCategoriesData.length < 1) return <FormLoading />;
// return <pre>{JSON.stringify(lessonCategoriesData, null, 2)}</pre>;
return ( return (
<Box <Box
sx={{ sx={{
@@ -94,7 +95,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
<Stack spacing={4}> <Stack spacing={4}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}> <Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
<Box sx={{ flex: '1 1 auto' }}> <Box sx={{ flex: '1 1 auto' }}>
<Typography variant="h4">{t('Lesson Categories')}</Typography> <Typography variant="h4">{t('dashboard.lessonCategorys.list.title')}</Typography>
</Box> </Box>
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}> <Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<LoadingButton <LoadingButton
@@ -115,7 +116,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
<LessonCategoriesFilters filters={{ email, phone, status: spStatus }} sortDir={sortDir} /> <LessonCategoriesFilters filters={{ email, phone, status: spStatus }} sortDir={sortDir} />
<Divider /> <Divider />
<Box sx={{ overflowX: 'auto' }}> <Box sx={{ overflowX: 'auto' }}>
<LessonCategoriesTable rows={filteredLessonCategories} /> <LessonCategoriesTable reloadRows={reloadRows} rows={filteredLessonCategories} />
</Box> </Box>
<Divider /> <Divider />
<LessonCategoriesPagination count={filteredLessonCategories.length + 100} page={0} /> <LessonCategoriesPagination count={filteredLessonCategories.length + 100} page={0} />
@@ -140,17 +141,17 @@ function applySort(row: LessonCategory[], sortDir: 'asc' | 'desc' | undefined):
function applyFilters(row: LessonCategory[], { email, phone, status }: Filters): LessonCategory[] { function applyFilters(row: LessonCategory[], { email, phone, status }: Filters): LessonCategory[] {
return row.filter((item) => { return row.filter((item) => {
if (email) { // if (email) {
if (!item.email?.toLowerCase().includes(email.toLowerCase())) { // if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
return false; // return false;
} // }
} // }
if (phone) { // if (phone) {
if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { // if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
return false; // return false;
} // }
} // }
if (status) { if (status) {
if (item.status !== status) { if (item.status !== status) {

View File

@@ -0,0 +1,123 @@
'use client';
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { COL_LESSON_TYPES } from '@/constants';
import { LoadingButton } from '@mui/lab';
import { Button, Container, Modal, Paper } from '@mui/material';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note';
import PocketBase from 'pocketbase';
import { useTranslation } from 'react-i18next';
import { logger } from '@/lib/default-logger';
import { toast } from '@/components/core/toaster';
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL);
export default function ConfirmDeleteModal({
open,
setOpen,
idToDelete,
reloadRows,
}: {
open: boolean;
setOpen: (b: boolean) => void;
idToDelete: string;
reloadRows: () => void;
}): React.JSX.Element {
const { t } = useTranslation();
// const handleClose = () => setOpen(false);
function handleClose(): void {
setOpen(false);
}
const [isDeleteing, setIsDeleteing] = React.useState(false);
const style = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
};
function 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) {
setIsDeleteing(true);
performDelete(idToDelete)
.then(() => {
handleClose();
setIsDeleteing(false);
})
.catch((err) => {
// console.error(err)
logger.error(err);
toast(t('dashboard.lessonTypes.delete.error'));
});
}
}
return (
<div>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<Container maxWidth="sm">
<Paper sx={{ border: '1px solid var(--mui-palette-divider)', boxShadow: 'var(--mui-shadows-16)' }}>
<Stack direction="row" spacing={2} sx={{ display: 'flex', p: 3 }}>
<Avatar sx={{ bgcolor: 'var(--mui-palette-error-50)', color: 'var(--mui-palette-error-main)' }}>
<NoteIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
<Stack spacing={3}>
<Stack spacing={1}>
<Typography variant="h5">{t('Delete Lesson Type ?')}</Typography>
<Typography color="text.secondary" variant="body2">
{t('Are you sure you want to delete lesson type ?')}
</Typography>
</Stack>
<Stack direction="row" spacing={2} sx={{ justifyContent: 'flex-end' }}>
<Button color="secondary" onClick={handleClose}>
{t('Cancel')}
</Button>
<LoadingButton
color="error"
variant="contained"
onClick={(e) => {
handleUserConfirmDelete();
}}
loading={isDeleteing}
>
{t('Delete')}
</LoadingButton>
</Stack>
</Stack>
</Stack>
</Paper>
</Container>
</Box>
</Modal>
</div>
);
}

View File

@@ -13,29 +13,15 @@ export interface LessonCategory {
description: string; description: string;
remarks: string; remarks: string;
// //
name?: string; name: string;
avatar?: string; avatar: string;
email?: string; email: string;
phone?: string; phone: string;
quota: number; quota: number;
status: 'pending' | 'active' | 'blocked' | 'NA'; status: 'pending' | 'active' | 'blocked' | 'NA';
createdAt: Date; createdAt: Date;
} }
export interface DBLessonCategory {
id: string;
cat_name: string;
cat_image_url?: string;
cat_image?: string;
pos: number;
visible: string;
lesson_id: string;
description: string;
remarks: string;
createdAt: Date;
status: string;
}
export const defaultLessonCategory: LessonCategory = { export const defaultLessonCategory: LessonCategory = {
id: 'default-id', id: 'default-id',
cat_name: 'default-category-name', cat_name: 'default-category-name',
@@ -46,8 +32,13 @@ export const defaultLessonCategory: LessonCategory = {
lesson_id: 'default-lesson-id', lesson_id: 'default-lesson-id',
description: 'default-description', description: 'default-description',
remarks: 'default-remarks', remarks: 'default-remarks',
//
createdAt: dayjs('2099-01-01').toDate(), createdAt: dayjs('2099-01-01').toDate(),
//
name: '',
avatar: '',
email: '',
phone: '',
quota: 0, quota: 0,
status: 'NA', status: 'NA',
}; };

View File

@@ -4,6 +4,7 @@ import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip'; import Chip from '@mui/material/Chip';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import LinearProgress from '@mui/material/LinearProgress'; import LinearProgress from '@mui/material/LinearProgress';
@@ -12,40 +13,48 @@ import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock';
import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images';
import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus';
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
import { DataTable } from '@/components/core/data-table'; import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table'; import type { ColumnDef } from '@/components/core/data-table';
import ConfirmDeleteModal from './confirm-delete-modal';
import type { LessonCategory } from './interfaces'; import type { LessonCategory } from './interfaces';
import { useLessonCategoriesSelection } from './lesson-categories-selection-context'; import { useLessonCategoriesSelection } from './lesson-categories-selection-context';
const columns = [ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonCategory>[] {
return [
{ {
formatter: (row): React.JSX.Element => ( formatter: (row): React.JSX.Element => (
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<Avatar src={row.avatar} />{' '}
<div>
<Link <Link
color="inherit" color="inherit"
component={RouterLink} component={RouterLink}
href={paths.dashboard.lesson_categories.details('1')} href={paths.dashboard.lesson_categories.details(row.id)}
sx={{ whiteSpace: 'nowrap' }} sx={{ whiteSpace: 'nowrap' }}
variant="subtitle2" variant="subtitle2"
> >
{row.name} <Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
</Link> <Avatar src={row.avatar} variant="rounded">
<ImagesIcon size={32} />
</Avatar>{' '}
<div>
<Box sx={{ whiteSpace: 'nowrap' }}>{row.cat_name}</Box>
<Typography color="text.secondary" variant="body2"> <Typography color="text.secondary" variant="body2">
{row.email} slug: {row.cat_name}
</Typography> </Typography>
</div> </div>
</Stack> </Stack>
</Link>
), ),
name: 'Name', name: 'Name',
width: '250px', width: '200px',
}, },
{ {
formatter: (row): React.JSX.Element => ( formatter: (row): React.JSX.Element => (
@@ -56,17 +65,13 @@ const columns = [
</Typography> </Typography>
</Stack> </Stack>
), ),
name: 'Quota', // NOTE: please refer to translation.json here
width: '250px', name: 'word-count',
}, width: '100px',
{ 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',
}, },
{ field: 'phone', name: 'Phone number', width: '100px' },
{ {
formatter: (row): React.JSX.Element => { formatter: (row): React.JSX.Element => {
const mapping = { const mapping = {
@@ -82,6 +87,13 @@ const columns = [
name: 'Status', name: 'Status',
width: '150px', width: '150px',
}, },
{
formatter(row) {
return dayjs(row.createdAt).format('MMM D, YYYY');
},
name: 'Created at',
width: '100px',
},
{ {
formatter: (): React.JSX.Element => ( formatter: (): React.JSX.Element => (
<IconButton component={RouterLink} href={paths.dashboard.lesson_categories.details('1')}> <IconButton component={RouterLink} href={paths.dashboard.lesson_categories.details('1')}>
@@ -93,19 +105,31 @@ const columns = [
width: '100px', width: '100px',
align: 'right', align: 'right',
}, },
] satisfies ColumnDef<LessonCategory>[]; ];
}
export interface LessonCategoriesTableProps { export interface LessonCategoriesTableProps {
rows: LessonCategory[]; rows: LessonCategory[];
reloadRows: () => void;
} }
export function LessonCategoriesTable({ rows }: LessonCategoriesTableProps): React.JSX.Element { export function LessonCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element {
const { t } = useTranslation();
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLessonCategoriesSelection(); const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLessonCategoriesSelection();
const [idToDelete, setIdToDelete] = React.useState('');
const [open, setOpen] = React.useState(false);
function handleDeleteClick(testId: string): void {
setOpen(true);
setIdToDelete(testId);
}
return ( return (
<React.Fragment> <React.Fragment>
<ConfirmDeleteModal idToDelete={idToDelete} open={open} reloadRows={reloadRows} setOpen={setOpen} />
<DataTable<LessonCategory> <DataTable<LessonCategory>
columns={columns} columns={columns(handleDeleteClick)}
onDeselectAll={deselectAll} onDeselectAll={deselectAll}
onDeselectOne={(_, row) => { onDeselectOne={(_, row) => {
deselectOne(row.id); deselectOne(row.id);
@@ -121,7 +145,8 @@ export function LessonCategoriesTable({ rows }: LessonCategoriesTableProps): Rea
{!rows.length ? ( {!rows.length ? (
<Box sx={{ p: 3 }}> <Box sx={{ p: 3 }}>
<Typography color="text.secondary" sx={{ textAlign: 'center' }} variant="body2"> <Typography color="text.secondary" sx={{ textAlign: 'center' }} variant="body2">
No lesson categories found {/* TODO: use hyphen here */}
{t('No lesson categories found')}
</Typography> </Typography>
</Box> </Box>
) : null} ) : null}

View File

@@ -2,11 +2,12 @@
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 Avatar from '@mui/material/Avatar';
import { Button } from '@mui/material';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip'; import Chip from '@mui/material/Chip';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import LinearProgress from '@mui/material/LinearProgress';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
@@ -20,7 +21,6 @@ import { toast } from 'sonner';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
import { i18n } from '@/lib/i18n';
import { DataTable } from '@/components/core/data-table'; import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table'; import type { ColumnDef } from '@/components/core/data-table';
@@ -196,6 +196,7 @@ export function LessonTypesTable({ rows, reloadRows }: LessonTypesTableProps): R
{!rows.length ? ( {!rows.length ? (
<Box sx={{ p: 3 }}> <Box sx={{ p: 3 }}>
<Typography color="text.secondary" sx={{ textAlign: 'center' }} variant="body2"> <Typography color="text.secondary" sx={{ textAlign: 'center' }} variant="body2">
{/* TODO: use hyphen here */}
{t('No lesson types found')} {t('No lesson types found')}
</Typography> </Typography>
</Box> </Box>