update build ok,
This commit is contained in:
@@ -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;
|
||||
});
|
||||
}
|
||||
|
@@ -1,3 +0,0 @@
|
||||
const helloworld = 'helloworld';
|
||||
|
||||
export { helloworld };
|
@@ -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;
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
const helloworld = 'helloworld';
|
||||
|
||||
export { helloworld };
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -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);
|
||||
}
|
Reference in New Issue
Block a user