diff --git a/002_source/cms/src/app/dashboard/lesson_categories/lesson-categories-sample-data.tsx b/002_source/cms/src/app/dashboard/lesson_categories/lesson-categories-sample-data.tsx
new file mode 100644
index 0000000..f44c56d
--- /dev/null
+++ b/002_source/cms/src/app/dashboard/lesson_categories/lesson-categories-sample-data.tsx
@@ -0,0 +1,56 @@
+import { dayjs } from '@/lib/dayjs';
+// import type { LessonCategory } from '@/components/dashboard/lesson_category/lesson-categories-table';
+import type { LessonCategory } from '@/components/dashboard/lesson_category/interfaces';
+
+export const lessonCategoriesSampleData = [
+ {
+ id: 'USR-005',
+ name: 'Fran Perez',
+ avatar: '/assets/avatar-5.png',
+ email: 'fran.perez@domain.com',
+ phone: '(815) 704-0045',
+ quota: 50,
+ status: 'active',
+ createdAt: dayjs().subtract(1, 'hour').toDate(),
+ },
+ {
+ id: 'USR-004',
+ name: 'Penjani Inyene',
+ avatar: '/assets/avatar-4.png',
+ email: 'penjani.inyene@domain.com',
+ phone: '(803) 937-8925',
+ quota: 100,
+ status: 'active',
+ createdAt: dayjs().subtract(3, 'hour').toDate(),
+ },
+ {
+ id: 'USR-003',
+ name: 'Carson Darrin',
+ avatar: '/assets/avatar-3.png',
+ email: 'carson.darrin@domain.com',
+ phone: '(715) 278-5041',
+ quota: 10,
+ status: 'blocked',
+ createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
+ },
+ {
+ id: 'USR-002',
+ name: 'Siegbert Gottfried',
+ avatar: '/assets/avatar-2.png',
+ email: 'siegbert.gottfried@domain.com',
+ phone: '(603) 766-0431',
+ quota: 0,
+ status: 'pending',
+ createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
+ },
+ {
+ id: 'USR-001',
+ name: 'Miron Vitold',
+ avatar: '/assets/avatar-1.png',
+ email: 'miron.vitold@domain.com',
+ phone: '(425) 434-5535',
+ quota: 50,
+ status: 'active',
+ createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
+ },
+] satisfies LessonCategory[];
diff --git a/002_source/cms/src/app/dashboard/lesson_categories/page.tsx b/002_source/cms/src/app/dashboard/lesson_categories/page.tsx
new file mode 100644
index 0000000..02c1520
--- /dev/null
+++ b/002_source/cms/src/app/dashboard/lesson_categories/page.tsx
@@ -0,0 +1,90 @@
+'use client';
+
+import * as React from 'react';
+import type { Metadata } from 'next';
+import Box from '@mui/material/Box';
+import Button from '@mui/material/Button';
+import Card from '@mui/material/Card';
+import Divider from '@mui/material/Divider';
+import Stack from '@mui/material/Stack';
+import Typography from '@mui/material/Typography';
+import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
+
+import { config } from '@/config';
+import type { LessonCategory } from '@/components/dashboard/lesson_category/interfaces';
+import { LessonCategoriesFilters } 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 { LessonCategoriesSelectionProvider } from '@/components/dashboard/lesson_category/lesson-categories-selection-context';
+import { LessonCategoriesTable } from '@/components/dashboard/lesson_category/lesson-categories-table';
+
+import { lessonCategoriesSampleData } from './lesson-categories-sample-data';
+
+interface PageProps {
+ searchParams: {
+ email?: string;
+ phone?: string;
+ sortDir?: 'asc' | 'desc';
+ status?: string;
+ name?: string;
+ visible?: string;
+ type?: string;
+ //
+ };
+}
+
+export default function Page({ searchParams }: PageProps): React.JSX.Element {
+ const { email, phone, sortDir, status } = searchParams;
+
+ const sortedLessonCategories = applySort(lessonCategoriesSampleData, sortDir);
+ const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status });
+
+ return (
+
+ Page
+
+ );
+}
+
+// Sorting and filtering has to be done on the server.
+
+function applySort(row: LessonCategory[], sortDir: 'asc' | 'desc' | undefined): LessonCategory[] {
+ 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: LessonCategory[], { email, phone, status }: Filters): LessonCategory[] {
+ 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;
+ }
+ }
+
+ return true;
+ });
+}
diff --git a/002_source/cms/src/components/dashboard/lesson_category/interfaces.ts b/002_source/cms/src/components/dashboard/lesson_category/interfaces.ts
new file mode 100644
index 0000000..72c8441
--- /dev/null
+++ b/002_source/cms/src/components/dashboard/lesson_category/interfaces.ts
@@ -0,0 +1,10 @@
+export interface LessonCategory {
+ id: string;
+ name: string;
+ avatar?: string;
+ email: string;
+ phone?: string;
+ quota: number;
+ status: 'pending' | 'active' | 'blocked';
+ createdAt: Date;
+}
diff --git a/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-filters.tsx b/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-filters.tsx
new file mode 100644
index 0000000..ab380d4
--- /dev/null
+++ b/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-filters.tsx
@@ -0,0 +1,244 @@
+'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 { paths } from '@/paths';
+import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
+import { Option } from '@/components/core/option';
+
+import { useLessonCategoriesSelection } from './lesson-categories-selection-context';
+
+// The tabs should be generated using API data.
+const tabs = [
+ { label: 'All', value: '', count: 5 },
+ { label: 'Active', value: 'active', count: 3 },
+ { label: 'Pending', value: 'pending', count: 1 },
+ { label: 'Blocked', value: 'blocked', count: 1 },
+] as const;
+
+export interface Filters {
+ email?: string;
+ phone?: string;
+ status?: string;
+}
+
+export type SortDir = 'asc' | 'desc';
+
+export interface LessonCategoriesFiltersProps {
+ filters?: Filters;
+ sortDir?: SortDir;
+}
+
+export function LessonCategoriesFilters({
+ filters = {},
+ sortDir = 'desc',
+}: LessonCategoriesFiltersProps): React.JSX.Element {
+ const { email, phone, status } = filters;
+
+ const router = useRouter();
+
+ const selection = useLessonCategoriesSelection();
+
+ 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);
+ }
+
+ router.push(`${paths.dashboard.lesson_categories.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 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;
+
+ return (
+
+
+ {tabs.map((tab) => (
+ }
+ iconPosition="end"
+ key={tab.value}
+ label={tab.label}
+ sx={{ minHeight: 'auto' }}
+ tabIndex={0}
+ value={tab.value}
+ />
+ ))}
+
+
+
+
+ {
+ 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} selected
+
+
+
+ ) : null}
+
+
+
+ );
+}
+
+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_category/lesson-categories-pagination.tsx b/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-pagination.tsx
new file mode 100644
index 0000000..a78bea3
--- /dev/null
+++ b/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-pagination.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+import * as React from 'react';
+import TablePagination from '@mui/material/TablePagination';
+
+function noop(): void {
+ return undefined;
+}
+
+interface LessonCategoriesPaginationProps {
+ count: number;
+ page: number;
+}
+
+export function LessonCategoriesPagination({ count, page }: LessonCategoriesPaginationProps): React.JSX.Element {
+ // You should implement the pagination using a similar logic as the filters.
+ // Note that when page change, you should keep the filter search params.
+
+ return (
+
+ );
+}
diff --git a/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-selection-context.tsx b/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-selection-context.tsx
new file mode 100644
index 0000000..af5f802
--- /dev/null
+++ b/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-selection-context.tsx
@@ -0,0 +1,47 @@
+'use client';
+
+import * as React from 'react';
+
+import { useSelection } from '@/hooks/use-selection';
+import type { Selection } from '@/hooks/use-selection';
+// import type { LessonCategory } from './lesson-categories-table';
+import type { LessonCategory } from '@/components/dashboard/lesson_category/interfaces';
+
+function noop(): void {
+ return undefined;
+}
+
+export interface LessonCategoriesSelectionContextValue extends Selection {}
+
+export const LessonCategoriesSelectionContext = React.createContext({
+ deselectAll: noop,
+ deselectOne: noop,
+ selectAll: noop,
+ selectOne: noop,
+ selected: new Set(),
+ selectedAny: false,
+ selectedAll: false,
+});
+
+interface LessonCategoriesSelectionProviderProps {
+ children: React.ReactNode;
+ lessonCategories: LessonCategory[];
+}
+
+export function LessonCategoriesSelectionProvider({
+ children,
+ lessonCategories = [],
+}: LessonCategoriesSelectionProviderProps): React.JSX.Element {
+ const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]);
+ const selection = useSelection(customerIds);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useLessonCategoriesSelection(): LessonCategoriesSelectionContextValue {
+ return React.useContext(LessonCategoriesSelectionContext);
+}
diff --git a/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-table.tsx b/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-table.tsx
new file mode 100644
index 0000000..12fdaed
--- /dev/null
+++ b/002_source/cms/src/components/dashboard/lesson_category/lesson-categories-table.tsx
@@ -0,0 +1,129 @@
+'use client';
+
+import * as React from 'react';
+import RouterLink from 'next/link';
+import Avatar from '@mui/material/Avatar';
+import Box from '@mui/material/Box';
+import Chip from '@mui/material/Chip';
+import IconButton from '@mui/material/IconButton';
+import LinearProgress from '@mui/material/LinearProgress';
+import Link from '@mui/material/Link';
+import Stack from '@mui/material/Stack';
+import Typography from '@mui/material/Typography';
+import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
+import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock';
+import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus';
+import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
+
+import { paths } from '@/paths';
+import { dayjs } from '@/lib/dayjs';
+import { DataTable } from '@/components/core/data-table';
+import type { ColumnDef } from '@/components/core/data-table';
+
+import { LessonCategory } from './interfaces';
+import { useLessonCategoriesSelection } from './lesson-categories-selection-context';
+
+const columns = [
+ {
+ formatter: (row): React.JSX.Element => (
+
+ {' '}
+
+
+ {row.name}
+
+
+ {row.email}
+
+
+
+ ),
+ name: 'Name',
+ width: '250px',
+ },
+ {
+ formatter: (row): React.JSX.Element => (
+
+
+
+ {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)}
+
+
+ ),
+ name: 'Quota',
+ width: '250px',
+ },
+ { 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',
+ },
+ {
+ formatter: (row): React.JSX.Element => {
+ const mapping = {
+ active: { label: 'Active', icon: },
+ blocked: { label: 'Blocked', icon: },
+ pending: { label: 'Pending', icon: },
+ } as const;
+ const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
+
+ return ;
+ },
+ name: 'Status',
+ width: '150px',
+ },
+ {
+ formatter: (): React.JSX.Element => (
+
+
+
+ ),
+ name: 'Actions',
+ hideName: true,
+ width: '100px',
+ align: 'right',
+ },
+] satisfies ColumnDef[];
+
+export interface LessonCategoriesTableProps {
+ rows: LessonCategory[];
+}
+
+export function LessonCategoriesTable({ rows }: LessonCategoriesTableProps): React.JSX.Element {
+ const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLessonCategoriesSelection();
+
+ return (
+
+
+ columns={columns}
+ onDeselectAll={deselectAll}
+ onDeselectOne={(_, row) => {
+ deselectOne(row.id);
+ }}
+ onSelectAll={selectAll}
+ onSelectOne={(_, row) => {
+ selectOne(row.id);
+ }}
+ rows={rows}
+ selectable
+ selected={selected}
+ />
+ {!rows.length ? (
+
+
+ No lesson categories found
+
+
+ ) : null}
+
+ );
+}