update build ok,

This commit is contained in:
louiscklaw
2025-04-16 01:00:30 +08:00
parent 0d6f97f5aa
commit e6980dceba
6 changed files with 595 additions and 7 deletions

View File

@@ -1,3 +1,5 @@
'use client';
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { LoadingButton } from '@mui/lab';
@@ -12,6 +14,10 @@ import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { toast } from '@/components/core/toaster';
import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType';
import type { Filters } from '@/components/dashboard/lesson_type/lesson-types-filters';
interface PageProps {
searchParams: {
@@ -27,7 +33,22 @@ interface PageProps {
}
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { t } = useTranslation();
const { email, phone, sortDir, status, name, visible, type } = searchParams;
const router = useRouter();
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
const [lessonTypesData, setLessonTypesData] = React.useState<LessonType[]>([]);
const sortedLessonTypes = applySort(lessonTypesData, sortDir);
const filteredLessonTypes = applyFilters(sortedLessonTypes, {
email,
phone,
status,
name,
type,
visible,
//
});
return (
<Box
@@ -38,7 +59,75 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
width: 'var(--Content-width)',
}}
>
hello lesson_type
<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('Lesson Types')}</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<LoadingButton
loading={isLoadingAddPage}
onClick={(): void => {
setIsLoadingAddPage(true);
router.push(paths.dashboard.lesson_types.create);
}}
startIcon={<PlusIcon />}
variant="contained"
>
{/* add new lesson type */}
{t('dashboard.lessonTypes.add')}
</LoadingButton>
</Box>
</Stack>
</Stack>
</Box>
);
}
// Sorting and filtering has to be done on the server.
function applySort(row: LessonType[], sortDir: 'asc' | 'desc' | undefined): LessonType[] {
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: LessonType[], { email, phone, status, name, visible }: Filters): LessonType[] {
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;
});
}

View File

@@ -1,3 +0,0 @@
const helloworld = 'helloworld';
export { helloworld };

View File

@@ -0,0 +1,57 @@
'use client';
import { dayjs } from '@/lib/dayjs';
export interface LessonType {
id: string;
name: string;
type: string;
pos: number;
visible: 'visible' | 'hidden';
createdAt: Date;
//
// original
// id: string;
// name: string;
//
avatar?: string;
email: string;
phone?: string;
quota: number;
status: 'pending' | 'active' | 'blocked';
// createdAt: Date;
}
export const LessonTypeDefaultValue: LessonType = {
id: 'string',
name: 'string',
type: 'string',
pos: 1,
visible: 'visible',
createdAt: dayjs().toDate(),
//
// original
// id: 'string',
// name: 'string',
//
avatar: 'string',
email: 'string',
phone: 'string',
quota: 1,
status: 'pending',
// createdAt: Date;
};
export interface DBLessonType {
id: string;
name: string;
type: string;
pos: number;
visible: 'visible' | 'hidden';
createdAt: Date;
created: 'string';
}
export interface Helloworld {
id: string;
}

View File

@@ -1,3 +0,0 @@
const helloworld = 'helloworld';
export { helloworld };

View File

@@ -0,0 +1,403 @@
'use client';
import * as React from 'react';
import { useRouter } from 'next/navigation';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
import FormControl from '@mui/material/FormControl';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import type { SelectChangeEvent } from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
import { Option } from '@/components/core/option';
import { LessonType } from './ILessonType';
import { useLessonTypesSelection } from './lesson-types-selection-context';
export interface Filters {
email?: string;
phone?: string;
status?: string;
name?: string;
visible?: string;
type?: string;
}
export type SortDir = 'asc' | 'desc';
export interface LessonTypesFiltersProps {
filters?: Filters;
sortDir?: SortDir;
fullData: LessonType[];
}
export function LessonTypesFilters({
filters = {},
sortDir = 'desc',
fullData,
}: LessonTypesFiltersProps): React.JSX.Element {
const { t } = useTranslation();
const { email, phone, status, name, visible, type } = filters;
const router = useRouter();
const selection = useLessonTypesSelection();
function getVisible(): number {
return fullData.reduce((count, item: LessonType) => {
return item.visible === 'visible' ? count + 1 : count;
}, 0);
}
function getHidden(): number {
return fullData.reduce((count, item: LessonType) => {
return item.visible === 'hidden' ? count + 1 : count;
}, 0);
}
// The tabs should be generated using API data.
const tabs = [
{ label: 'All', value: '', count: fullData.length },
// { label: 'Active', value: 'active', count: 3 },
// { label: 'Pending', value: 'pending', count: 1 },
// { label: 'Blocked', value: 'blocked', count: 1 },
{ label: t('visible'), value: 'visible', count: getVisible() },
{ label: t('hidden'), value: 'hidden', count: getHidden() },
] as const;
const updateSearchParams = React.useCallback(
(newFilters: Filters, newSortDir: SortDir): void => {
const searchParams = new URLSearchParams();
if (newSortDir === 'asc') {
searchParams.set('sortDir', newSortDir);
}
if (newFilters.status) {
searchParams.set('status', newFilters.status);
}
if (newFilters.email) {
searchParams.set('email', newFilters.email);
}
if (newFilters.phone) {
searchParams.set('phone', newFilters.phone);
}
if (newFilters.name) {
searchParams.set('name', newFilters.name);
}
if (newFilters.type) {
searchParams.set('type', newFilters.type);
}
if (newFilters.visible) {
searchParams.set('visible', newFilters.visible);
}
router.push(`${paths.dashboard.lesson_types.list}?${searchParams.toString()}`);
},
[router]
);
const handleClearFilters = React.useCallback(() => {
updateSearchParams({}, sortDir);
}, [updateSearchParams, sortDir]);
const handleStatusChange = React.useCallback(
(_: React.SyntheticEvent, value: string) => {
updateSearchParams({ ...filters, status: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleVisibleChange = React.useCallback(
(_: React.SyntheticEvent, value: string) => {
updateSearchParams({ ...filters, visible: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleNameChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, name: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleTypeChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, type: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleEmailChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, email: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handlePhoneChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, phone: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleSortChange = React.useCallback(
(event: SelectChangeEvent) => {
updateSearchParams(filters, event.target.value as SortDir);
},
[updateSearchParams, filters]
);
const hasFilters = status || email || phone || visible || name || type;
return (
<div>
<Tabs onChange={handleVisibleChange} sx={{ px: 3 }} value={visible ?? ''} variant="scrollable">
{tabs.map((tab) => (
<Tab
icon={<Chip label={tab.count} size="small" variant="soft" />}
iconPosition="end"
key={tab.value}
label={tab.label}
sx={{ minHeight: 'auto' }}
tabIndex={0}
value={tab.value}
/>
))}
</Tabs>
<Divider />
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap', px: 3, py: 2 }}>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto', flexWrap: 'wrap' }}>
<FilterButton
displayValue={name}
label={t('Name')}
onFilterApply={(value) => {
handleNameChange(value as string);
}}
onFilterDelete={() => {
handleNameChange();
}}
popover={<NameFilterPopover />}
value={name}
/>
<FilterButton
displayValue={type}
label={t('Type')}
onFilterApply={(value) => {
handleTypeChange(value as string);
}}
onFilterDelete={() => {
handleTypeChange();
}}
popover={<TypeFilterPopover />}
value={type}
/>
{/*
<FilterButton
displayValue={email}
label="Email"
onFilterApply={(value) => {
handleEmailChange(value as string);
}}
onFilterDelete={() => {
handleEmailChange();
}}
popover={<EmailFilterPopover />}
value={email}
/>
*/}
{/*
<FilterButton
displayValue={phone}
label="Phone number"
onFilterApply={(value) => {
handlePhoneChange(value as string);
}}
onFilterDelete={() => {
handlePhoneChange();
}}
popover={<PhoneFilterPopover />}
value={phone}
/>
*/}
{hasFilters ? <Button onClick={handleClearFilters}>{t('Clear filters')}</Button> : null}
</Stack>
{selection.selectedAny ? (
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<Typography color="text.secondary" variant="body2">
{selection.selected.size} {t('selected')}
</Typography>
<Button color="error" variant="contained">
{t('Delete')}
</Button>
</Stack>
) : null}
<Select name="sort" onChange={handleSortChange} sx={{ maxWidth: '100%', width: '120px' }} value={sortDir}>
<Option value="desc">{t('Newest')}</Option>
<Option value="asc">{t('Oldest')}</Option>
</Select>
</Stack>
</div>
);
}
function TypeFilterPopover(): React.JSX.Element {
const { t } = useTranslation();
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title={t('Filter by type')}>
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
{t('Apply')}
</Button>
</FilterPopover>
);
}
function NameFilterPopover(): React.JSX.Element {
const { t } = useTranslation();
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title={t('Filter by name')}>
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
{t('Apply')}
</Button>
</FilterPopover>
);
}
function EmailFilterPopover(): React.JSX.Element {
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title="Filter by email">
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
Apply
</Button>
</FilterPopover>
);
}
function PhoneFilterPopover(): React.JSX.Element {
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover anchorEl={anchorEl} onClose={onClose} open={open} title="Filter by phone number">
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
Apply
</Button>
</FilterPopover>
);
}

View File

@@ -0,0 +1,45 @@
'use client';
import * as React from 'react';
import { useSelection } from '@/hooks/use-selection';
import type { Selection } from '@/hooks/use-selection';
import { LessonType } from './ILessonType';
function noop(): void {
return undefined;
}
export interface LessonTypesSelectionContextValue extends Selection {}
export const LessonTypesSelectionContext = React.createContext<LessonTypesSelectionContextValue>({
deselectAll: noop,
deselectOne: noop,
selectAll: noop,
selectOne: noop,
selected: new Set(),
selectedAny: false,
selectedAll: false,
});
interface LessonTypesSelectionProviderProps {
children: React.ReactNode;
lessonTypes: LessonType[];
}
export function LessonTypesSelectionProvider({
children,
lessonTypes = [],
}: LessonTypesSelectionProviderProps): React.JSX.Element {
const lessonTypeIds = React.useMemo(() => lessonTypes.map((lessonType) => lessonType.id), [lessonTypes]);
const selection = useSelection(lessonTypeIds);
return (
<LessonTypesSelectionContext.Provider value={{ ...selection }}>{children}</LessonTypesSelectionContext.Provider>
);
}
export function useLessonTypesSelection(): LessonTypesSelectionContextValue {
return React.useContext(LessonTypesSelectionContext);
}