diff --git a/002_source/cms/src/app/dashboard/lesson_types/page.tsx b/002_source/cms/src/app/dashboard/lesson_types/page.tsx index 262a7c2..8ae82b5 100644 --- a/002_source/cms/src/app/dashboard/lesson_types/page.tsx +++ b/002_source/cms/src/app/dashboard/lesson_types/page.tsx @@ -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(false); + const [lessonTypesData, setLessonTypesData] = React.useState([]); + const sortedLessonTypes = applySort(lessonTypesData, sortDir); + const filteredLessonTypes = applyFilters(sortedLessonTypes, { + email, + phone, + status, + name, + type, + visible, + // + }); return ( - hello lesson_type + + + + {t('Lesson Types')} + + + { + setIsLoadingAddPage(true); + router.push(paths.dashboard.lesson_types.create); + }} + startIcon={} + variant="contained" + > + {/* add new lesson type */} + {t('dashboard.lessonTypes.add')} + + + + ); } + +// 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; + }); +} diff --git a/002_source/cms/src/components/dashboard/lesson_category/helloworld.tsx b/002_source/cms/src/components/dashboard/lesson_category/helloworld.tsx deleted file mode 100644 index 3989cb1..0000000 --- a/002_source/cms/src/components/dashboard/lesson_category/helloworld.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const helloworld = 'helloworld'; - -export { helloworld }; diff --git a/002_source/cms/src/components/dashboard/lesson_type/ILessonType.tsx b/002_source/cms/src/components/dashboard/lesson_type/ILessonType.tsx new file mode 100644 index 0000000..65a4930 --- /dev/null +++ b/002_source/cms/src/components/dashboard/lesson_type/ILessonType.tsx @@ -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; +} diff --git a/002_source/cms/src/components/dashboard/lesson_type/helloworld.tsx b/002_source/cms/src/components/dashboard/lesson_type/helloworld.tsx deleted file mode 100644 index 3989cb1..0000000 --- a/002_source/cms/src/components/dashboard/lesson_type/helloworld.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const helloworld = 'helloworld'; - -export { helloworld }; diff --git a/002_source/cms/src/components/dashboard/lesson_type/lesson-types-filters.tsx b/002_source/cms/src/components/dashboard/lesson_type/lesson-types-filters.tsx new file mode 100644 index 0000000..abb6f42 --- /dev/null +++ b/002_source/cms/src/components/dashboard/lesson_type/lesson-types-filters.tsx @@ -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 ( +
+ + {tabs.map((tab) => ( + } + iconPosition="end" + key={tab.value} + label={tab.label} + sx={{ minHeight: 'auto' }} + tabIndex={0} + value={tab.value} + /> + ))} + + + + + { + handleNameChange(value as string); + }} + onFilterDelete={() => { + handleNameChange(); + }} + popover={} + value={name} + /> + + { + handleTypeChange(value as string); + }} + onFilterDelete={() => { + handleTypeChange(); + }} + popover={} + value={type} + /> + + {/* + { + handleEmailChange(value as string); + }} + onFilterDelete={() => { + handleEmailChange(); + }} + popover={} + value={email} + /> + */} + + {/* + { + handlePhoneChange(value as string); + }} + onFilterDelete={() => { + handlePhoneChange(); + }} + popover={} + value={phone} + /> + */} + + {hasFilters ? : null} + + {selection.selectedAny ? ( + + + {selection.selected.size} {t('selected')} + + + + ) : null} + + +
+ ); +} + +function TypeFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function NameFilterPopover(): React.JSX.Element { + const { t } = useTranslation(); + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function EmailFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function PhoneFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} diff --git a/002_source/cms/src/components/dashboard/lesson_type/lesson-types-selection-context.tsx b/002_source/cms/src/components/dashboard/lesson_type/lesson-types-selection-context.tsx new file mode 100644 index 0000000..e492466 --- /dev/null +++ b/002_source/cms/src/components/dashboard/lesson_type/lesson-types-selection-context.tsx @@ -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({ + 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 ( + {children} + ); +} + +export function useLessonTypesSelection(): LessonTypesSelectionContextValue { + return React.useContext(LessonTypesSelectionContext); +}