diff --git a/002_source/cms/src/components/dashboard/lp_categories/_PROMPT.MD b/002_source/cms/src/components/dashboard/lp_categories/_PROMPT.MD
deleted file mode 100644
index 93ce5a6..0000000
--- a/002_source/cms/src/components/dashboard/lp_categories/_PROMPT.MD
+++ /dev/null
@@ -1 +0,0 @@
-please review and add translations, e.g. `{t('[word]')}`
diff --git a/002_source/cms/src/components/dashboard/lp_categories/_constants.ts b/002_source/cms/src/components/dashboard/lp_categories/_constants.ts
deleted file mode 100644
index 885067e..0000000
--- a/002_source/cms/src/components/dashboard/lp_categories/_constants.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { dayjs } from '@/lib/dayjs';
-
-import { CreateFormProps, LpCategory } from './type';
-
-export const defaultLpCategory: LpCategory = {
- isEmpty: false,
- id: 'default-id',
- cat_name: 'default-category-name',
- cat_image_url: undefined,
- cat_image: undefined,
- pos: 0,
- visible: 'hidden',
- lesson_id: 'default-lesson-id',
- description: 'default-description',
- remarks: 'default-remarks',
- slug: '',
- init_answer: {},
- // from pocketbase
- collectionId: '0000000000',
- createdAt: dayjs('2099-01-01').toDate(),
- //
- name: '',
- avatar: '',
- email: '',
- phone: '',
- quota: 0,
- status: 'NA',
-};
-
-// export const LpCategoryCreateFormDefault: CreateFormProps = {
-// name: '',
-// type: '',
-// pos: 1,
-// visible: 'visible',
-// description: '',
-// isActive: true,
-// order: 1,
-// imageUrl: '',
-// };
-
-export const emptyLpCategory: LpCategory = {
- ...defaultLpCategory,
- isEmpty: true,
-};
diff --git a/002_source/cms/src/components/dashboard/lp_categories/confirm-delete-modal.tsx b/002_source/cms/src/components/dashboard/lp_categories/confirm-delete-modal.tsx
deleted file mode 100644
index 4d5b267..0000000
--- a/002_source/cms/src/components/dashboard/lp_categories/confirm-delete-modal.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import { useRouter } from 'next/navigation';
-import { COL_LESSON_TYPES } from '@/constants';
-import deleteQuizLPCategories from '@/db/QuizListenings/Delete';
-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';
-
-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 handleUserConfirmDelete(): void {
- if (idToDelete) {
- setIsDeleteing(true);
- deleteQuizLPCategories(idToDelete)
- .then(() => {
- reloadRows();
- handleClose();
- toast(t('delete.success'));
- })
- .catch((err) => {
- // console.error(err)
- logger.error(err);
- toast(t('delete.error'));
- })
- .finally(() => {
- setIsDeleteing(false);
- });
- }
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- {t('Delete Lesson Type ?')}
-
- {t('Are you sure you want to delete lesson type ?')}
-
-
-
-
- {
- handleUserConfirmDelete();
- }}
- loading={isDeleteing}
- >
- {t('Delete')}
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-filters.tsx b/002_source/cms/src/components/dashboard/lp_categories/lp-categories-filters.tsx
deleted file mode 100644
index 7573cc6..0000000
--- a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-filters.tsx
+++ /dev/null
@@ -1,456 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import { useRouter } from 'next/navigation';
-// import { COL_LESSON_CATEGORIES } from '@/constants';
-import GetAllCount from '@/db/QuizListenings/GetAllCount';
-import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount';
-import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount';
-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 { useLpCategoriesSelection } from './lp-categories-selection-context';
-import { LpCategory } from './type';
-
-export interface Filters {
- email?: string;
- phone?: string;
- status?: string;
- name?: string;
- visible?: string;
- type?: string;
-}
-
-export type SortDir = 'asc' | 'desc';
-
-export interface LpCategoriesFiltersProps {
- filters?: Filters;
- sortDir?: SortDir;
- fullData: LpCategory[];
-}
-
-export function LpCategoriesFilters({
- filters = {},
- sortDir = 'desc',
- fullData,
-}: LpCategoriesFiltersProps): React.JSX.Element {
- const { t } = useTranslation();
- const { email, phone, status, name, visible, type } = filters;
-
- const [totalCount, setTotalCount] = React.useState(0);
- const [visibleCount, setVisibleCount] = React.useState(0);
- const [hiddenCount, setHiddenCount] = React.useState(0);
-
- const router = useRouter();
-
- const selection = useLpCategoriesSelection();
-
- function getVisible(): number {
- return fullData.reduce((count, item: LpCategory) => {
- return item.visible === 'visible' ? count + 1 : count;
- }, 0);
- }
-
- function getHidden(): number {
- return fullData.reduce((count, item: LpCategory) => {
- return item.visible === 'hidden' ? count + 1 : count;
- }, 0);
- }
-
- // The tabs should be generated using API data.
- const tabs = [
- { label: t('All'), value: '', count: totalCount },
- // { label: 'Active', value: 'active', count: 3 },
- // { label: 'Pending', value: 'pending', count: 1 },
- // { label: 'Blocked', value: 'blocked', count: 1 },
- { label: t('visible'), value: 'visible', count: visibleCount },
- { label: t('hidden'), value: 'hidden', count: hiddenCount },
- ] 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);
- }
-
- // NOTE: modify according to COLLECTION
- router.push(`${paths.dashboard.lp_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 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]
- );
-
- React.useEffect(() => {
- const fetchCount = async (): Promise => {
- try {
- const tc = await GetAllCount();
- setTotalCount(tc);
-
- const vc = await GetVisibleCount();
- setVisibleCount(vc);
-
- const hc = await GetHiddenCount();
- setHiddenCount(hc);
- } catch (error) {
- //
- }
- };
- void fetchCount();
- }, []);
-
- 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}
- />
-
- {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('');
- const { t } = useTranslation();
-
- 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('');
- const { t } = useTranslation();
-
- 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/lp_categories/lp-categories-pagination.tsx b/002_source/cms/src/components/dashboard/lp_categories/lp-categories-pagination.tsx
deleted file mode 100644
index bf40481..0000000
--- a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-pagination.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import TablePagination from '@mui/material/TablePagination';
-
-function noop(): void {
- return undefined;
-}
-
-interface LessonCategoriesPaginationProps {
- count: number;
- page: number;
- //
- setPage: (page: number) => void;
- setRowsPerPage: (page: number) => void;
- rowsPerPage: number;
-}
-
-export function LpCategoriesPagination({
- count,
- page,
- //
- setPage,
- setRowsPerPage,
- rowsPerPage,
-}: 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.
- const handleChangePage = (event: unknown, newPage: number) => {
- setPage(newPage);
- };
-
- const handleChangeRowsPerPage = (event: React.ChangeEvent) => {
- setRowsPerPage(parseInt(event.target.value));
- // console.log(parseInt(event.target.value));
- };
-
- return (
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-selection-context.tsx b/002_source/cms/src/components/dashboard/lp_categories/lp-categories-selection-context.tsx
deleted file mode 100644
index a6c6502..0000000
--- a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-selection-context.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-'use client';
-
-import * as React from 'react';
-
-// import type { LessonCategory } from '@/types/lesson-type';
-import { useSelection } from '@/hooks/use-selection';
-import type { Selection } from '@/hooks/use-selection';
-
-import { LpCategory } from './type';
-
-function noop(): void {
- return undefined;
-}
-
-export interface LpCategoriesSelectionContextValue extends Selection {}
-
-export const LpCategoriesSelectionContext = React.createContext({
- deselectAll: noop,
- deselectOne: noop,
- selectAll: noop,
- selectOne: noop,
- selected: new Set(),
- selectedAny: false,
- selectedAll: false,
-});
-
-interface LpCategoriesSelectionProviderProps {
- children: React.ReactNode;
- lessonCategories: LpCategory[];
-}
-
-export function LpCategoriesSelectionProvider({
- children,
- lessonCategories = [],
-}: LpCategoriesSelectionProviderProps): React.JSX.Element {
- const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]);
- const selection = useSelection(customerIds);
-
- return (
- {children}
- );
-}
-
-export function useLpCategoriesSelection(): LpCategoriesSelectionContextValue {
- return React.useContext(LpCategoriesSelectionContext);
-}
diff --git a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-table.tsx b/002_source/cms/src/components/dashboard/lp_categories/lp-categories-table.tsx
deleted file mode 100644
index 26034f1..0000000
--- a/002_source/cms/src/components/dashboard/lp_categories/lp-categories-table.tsx
+++ /dev/null
@@ -1,241 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import RouterLink from 'next/link';
-import { LoadingButton } from '@mui/lab';
-import Avatar from '@mui/material/Avatar';
-import Box from '@mui/material/Box';
-import Button from '@mui/material/Button';
-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 { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images';
-import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus';
-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 { dayjs } from '@/lib/dayjs';
-import { DataTable } from '@/components/core/data-table';
-import type { ColumnDef } from '@/components/core/data-table';
-
-import ConfirmDeleteModal from './confirm-delete-modal';
-import { useLpCategoriesSelection } from './lp-categories-selection-context';
-import type { LpCategory } from './type';
-
-function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] {
- return [
- {
- formatter: (row): React.JSX.Element => (
-
-
-
-
-
- {' '}
-
- {row.cat_name}
-
- slug: {row.cat_name}
-
-
-
-
-
- ),
- name: 'Name',
- width: '200px',
- },
- {
- formatter: (row): React.JSX.Element => (
-
-
-
- {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)}
-
-
- ),
- // NOTE: please refer to translation.json here
- name: 'word-count',
- width: '100px',
- },
- {
- formatter: (row): React.JSX.Element => {
- // eslint-disable-next-line react-hooks/rules-of-hooks
-
- const mapping = {
- active: {
- label: 'Active',
- icon: (
-
- ),
- },
- blocked: { label: 'Blocked', icon: },
- pending: {
- label: 'Pending',
- icon: (
-
- ),
- },
- NA: {
- label: 'NA',
- icon: (
-
- ),
- },
- } as const;
- const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
-
- return (
-
- );
- },
- name: 'Status',
- width: '150px',
- },
- {
- formatter(row) {
- return dayjs(row.createdAt).format('MMM D, YYYY');
- },
- name: 'Created at',
- width: '100px',
- },
- {
- formatter: (row): React.JSX.Element => (
-
-
-
-
- {
- handleDeleteClick(row.id);
- }}
- >
-
-
-
- ),
- name: 'Actions',
- hideName: true,
- width: '100px',
- align: 'right',
- },
- ];
-}
-
-export interface LessonCategoriesTableProps {
- rows: LpCategory[];
- reloadRows: () => void;
-}
-
-export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element {
- const { t } = useTranslation(['lp_categories']);
- const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpCategoriesSelection();
-
- const [idToDelete, setIdToDelete] = React.useState('');
- const [open, setOpen] = React.useState(false);
-
- function handleDeleteClick(testId: string): void {
- setOpen(true);
- setIdToDelete(testId);
- }
-
- return (
-
-
-
- columns={columns(handleDeleteClick)}
- onDeselectAll={deselectAll}
- onDeselectOne={(_, row) => {
- deselectOne(row.id);
- }}
- onSelectAll={selectAll}
- onSelectOne={(_, row) => {
- selectOne(row.id);
- }}
- rows={rows}
- selectable
- selected={selected}
- />
- {!rows.length ? (
-
-
- {t('no-lesson-categories-found')}
-
-
- ) : null}
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_categories/lp-category-create-form.tsx b/002_source/cms/src/components/dashboard/lp_categories/lp-category-create-form.tsx
deleted file mode 100644
index ba46ac7..0000000
--- a/002_source/cms/src/components/dashboard/lp_categories/lp-category-create-form.tsx
+++ /dev/null
@@ -1,419 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import RouterLink from 'next/link';
-import { useRouter } from 'next/navigation';
-import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { LoadingButton } from '@mui/lab';
-import { Avatar, Divider, MenuItem, Select } from '@mui/material';
-// import Avatar from '@mui/material/Avatar';
-import Box from '@mui/material/Box';
-import Button from '@mui/material/Button';
-import Card from '@mui/material/Card';
-import CardActions from '@mui/material/CardActions';
-import CardContent from '@mui/material/CardContent';
-import FormControl from '@mui/material/FormControl';
-import FormHelperText from '@mui/material/FormHelperText';
-import InputLabel from '@mui/material/InputLabel';
-import OutlinedInput from '@mui/material/OutlinedInput';
-import Stack from '@mui/material/Stack';
-import Typography from '@mui/material/Typography';
-import Grid from '@mui/material/Unstable_Grid2';
-import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
-// import axios from 'axios';
-import { Controller, useForm } from 'react-hook-form';
-import { useTranslation } from 'react-i18next';
-import { z as zod } from 'zod';
-
-import { paths } from '@/paths';
-import isDevelopment from '@/lib/check-is-development';
-import { logger } from '@/lib/default-logger';
-import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
-import { pb } from '@/lib/pb';
-import { TextEditor } from '@/components/core/text-editor/text-editor';
-import { toast } from '@/components/core/toaster';
-
-import type { CreateFormProps } from './type';
-
-const schema = zod.object({
- cat_name: zod.string().min(1, 'name-is-required').max(255),
- cat_image: zod.array(zod.any()).optional(),
- pos: zod.number().min(1, 'position is required').max(99),
- init_answer: zod.string().optional(),
- visible: zod.string(),
- slug: zod.string().min(1, 'slug-is-required').max(255),
- remarks: zod.string().optional(),
- description: zod.string().optional(),
- // NOTE: for image handling
- avatar: zod.string().optional(),
- // TODO: remove me
- type: zod.string().optional(),
- isActive: zod.boolean().optional(),
- order: zod.number().optional(),
-});
-
-type Values = zod.infer;
-
-export const defaultValues = {
- cat_name: '',
- cat_image: undefined,
- pos: 1,
- init_answer: '',
- visible: 'hidden',
- slug: '',
- remarks: '',
- description: '',
-} satisfies Values;
-
-export function LpCategoryCreateForm(): React.JSX.Element {
- const router = useRouter();
- const { t } = useTranslation(['lp_categories']);
-
- const [isCreating, setIsCreating] = React.useState(false);
-
- const {
- control,
- handleSubmit,
- formState: { errors },
- setValue,
- watch,
- } = useForm({ defaultValues, resolver: zodResolver(schema) });
-
- const onSubmit = React.useCallback(
- async (values: Values): Promise => {
- setIsCreating(true);
-
- const payload: CreateFormProps = {
- cat_name: values.cat_name,
- cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null,
- pos: values.pos,
- init_answer: values.init_answer,
- visible: values.visible,
- slug: values.slug,
- remarks: values.remarks,
- description: values.description,
- //
- // TODO: remove me
- type: 'type tet',
- isActive: true,
- order: 1,
- };
-
- try {
- const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).create(payload);
-
- logger.debug(result);
- toast.success(t('create.success'));
- router.push(paths.dashboard.lp_categories.list);
- } catch (error) {
- logger.error(error);
- toast.error(t('create.failed'));
- } finally {
- setIsCreating(false);
- }
- },
- // t is not necessary here
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [router]
- );
-
- const avatarInputRef = React.useRef(null);
- const avatar = watch('avatar');
-
- const handleAvatarChange = React.useCallback(
- async (event: React.ChangeEvent) => {
- const file = event.target.files?.[0];
-
- if (file) {
- const url = await fileToBase64(file);
- setValue('avatar', url);
- }
- },
- [setValue]
- );
-
- return (
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_categories/lp-category-edit-form.tsx b/002_source/cms/src/components/dashboard/lp_categories/lp-category-edit-form.tsx
deleted file mode 100644
index ba4020d..0000000
--- a/002_source/cms/src/components/dashboard/lp_categories/lp-category-edit-form.tsx
+++ /dev/null
@@ -1,500 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import RouterLink from 'next/link';
-import { useParams, useRouter } from 'next/navigation';
-//
-import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { LoadingButton } from '@mui/lab';
-//
-import Avatar from '@mui/material/Avatar';
-import Box from '@mui/material/Box';
-import Button from '@mui/material/Button';
-import Card from '@mui/material/Card';
-import CardActions from '@mui/material/CardActions';
-import CardContent from '@mui/material/CardContent';
-import Divider from '@mui/material/Divider';
-import FormControl from '@mui/material/FormControl';
-import FormHelperText from '@mui/material/FormHelperText';
-import InputLabel from '@mui/material/InputLabel';
-import MenuItem from '@mui/material/MenuItem';
-import OutlinedInput from '@mui/material/OutlinedInput';
-import Select from '@mui/material/Select';
-import Stack from '@mui/material/Stack';
-import Typography from '@mui/material/Typography';
-import Grid from '@mui/material/Unstable_Grid2';
-//
-import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
-//
-import { Controller, useForm } from 'react-hook-form';
-import { useTranslation } from 'react-i18next';
-import { z as zod } from 'zod';
-
-import { paths } from '@/paths';
-import { logger } from '@/lib/default-logger';
-import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
-import { pb } from '@/lib/pb';
-import { TextEditor } from '@/components/core/text-editor/text-editor';
-import { toast } from '@/components/core/toaster';
-import FormLoading from '@/components/loading';
-
-import ErrorDisplay from '../error';
-import type { EditFormProps } from './type';
-
-// TODO: review this
-const schema = zod.object({
- cat_name: zod.string().min(1, 'name-is-required').max(255),
- // accept file object when user change image
- // accept http string when user not changing image
- cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(),
-
- // position
- pos: zod.number().min(1, 'position is required').max(99),
-
- // it should be a valid JSON
- init_answer: zod
- .string()
- .refine(
- (value) => {
- try {
- JSON.parse(value);
- return true;
- } catch (error) {
- return false;
- }
- },
- { message: 'init_answer must be a valid JSON' }
- )
- .optional(),
- visible: zod.string(),
- slug: zod.string().min(0, 'slug-is-required').max(255).optional(),
- remarks: zod.string().optional(),
- description: zod.string().optional(),
- // NOTE: for image handling
- avatar: zod.string().optional(),
-});
-
-type Values = zod.infer;
-
-const defaultValues = {
- cat_name: '',
- cat_image: undefined,
- pos: 1,
- init_answer: JSON.stringify({}),
- visible: 'hidden',
- slug: '',
- remarks: '',
- description: '',
-} satisfies Values;
-
-export function LpCategoryEditForm(): React.JSX.Element {
- const router = useRouter();
- const { t } = useTranslation(['lp_categories']);
-
- const { cat_id: catId } = useParams<{ cat_id: string }>();
- //
- const [isUpdating, setIsUpdating] = React.useState(false);
- const [showLoading, setShowLoading] = React.useState(false);
- //
- const [showError, setShowError] = React.useState({ show: false, detail: '' });
-
- const {
- control,
- handleSubmit,
- formState: { errors },
- setValue,
- reset,
- watch,
- } = useForm({ defaultValues, resolver: zodResolver(schema) });
-
- const onSubmit = React.useCallback(
- async (values: Values): Promise => {
- setIsUpdating(true);
-
- const tempUpdate: EditFormProps = {
- cat_name: values.cat_name,
- cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null,
- pos: values.pos,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- init_answer: JSON.parse(values.init_answer || '{}'),
-
- visible: values.visible,
- slug: values.slug || 'not-defined',
- remarks: values.remarks,
- description: values.description,
- //
- // TODO: remove below
- type: '',
- };
-
- try {
- const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate);
- logger.debug(result);
- toast.success(t('edit.success'));
- router.push(paths.dashboard.lp_categories.list);
- } catch (error) {
- logger.error(error);
- toast.error(t('update.failed'));
- } finally {
- setIsUpdating(false);
- }
- },
- // t is not necessary here
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [router]
- );
-
- const avatarInputRef = React.useRef(null);
- const avatar = watch('avatar');
-
- const handleAvatarChange = React.useCallback(
- async (event: React.ChangeEvent) => {
- const file = event.target.files?.[0];
-
- if (file) {
- const url = await fileToBase64(file);
- setValue('avatar', url);
- }
- },
- [setValue]
- );
-
- // TODO: need to align with save form
- // use trycatch
- const [textDescription, setTextDescription] = React.useState('');
- const [textRemarks, setTextRemarks] = React.useState('');
-
- // load existing data when user arrive
- const loadExistingData = React.useCallback(
- async (id: string) => {
- setShowLoading(true);
-
- try {
- const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id);
-
- reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) });
- setTextDescription(result.description);
- setTextRemarks(result.remarks);
-
- if (result.cat_image !== '') {
- const fetchResult = await fetch(
- `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}`
- );
-
- const blob = await fetchResult.blob();
- const url = await fileToBase64(blob);
-
- setValue('avatar', url);
- } else {
- setValue('avatar', '');
- }
- } catch (error) {
- logger.error(error);
- toast(t('list.error'));
-
- setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
- } finally {
- setShowLoading(false);
- }
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [catId]
- );
-
- React.useEffect(() => {
- void loadExistingData(catId);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [catId]);
-
- if (showLoading) return ;
- if (showError.show)
- return (
-
- );
-
- return (
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_categories/notifications.tsx b/002_source/cms/src/components/dashboard/lp_categories/notifications.tsx
deleted file mode 100644
index a6c16bd..0000000
--- a/002_source/cms/src/components/dashboard/lp_categories/notifications.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import Avatar from '@mui/material/Avatar';
-import Box from '@mui/material/Box';
-import Button from '@mui/material/Button';
-import Card from '@mui/material/Card';
-import CardContent from '@mui/material/CardContent';
-import CardHeader from '@mui/material/CardHeader';
-import Chip from '@mui/material/Chip';
-import Select from '@mui/material/Select';
-import Stack from '@mui/material/Stack';
-import Typography from '@mui/material/Typography';
-import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple';
-
-import { dayjs } from '@/lib/dayjs';
-import { DataTable } from '@/components/core/data-table';
-import type { ColumnDef } from '@/components/core/data-table';
-import { Option } from '@/components/core/option';
-
-export interface Notification {
- id: string;
- type: string;
- status: 'delivered' | 'pending' | 'failed';
- createdAt: Date;
-}
-
-const columns = [
- {
- formatter: (row): React.JSX.Element => (
-
- {row.type}
-
- ),
- name: 'Type',
- width: '300px',
- },
- {
- formatter: (row): React.JSX.Element => {
- const mapping = {
- delivered: { label: 'Delivered', color: 'success' },
- pending: { label: 'Pending', color: 'warning' },
- failed: { label: 'Failed', color: 'error' },
- } as const;
- const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' };
-
- return ;
- },
- name: 'Status',
- width: '200px',
- },
- {
- formatter: (row): React.JSX.Element => (
-
- {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')}
-
- ),
- name: 'Date',
- align: 'right',
- },
-] satisfies ColumnDef[];
-
-export interface NotificationsProps {
- notifications: Notification[];
-}
-
-export function Notifications({ notifications }: NotificationsProps): React.JSX.Element {
- return (
-
-
-
-
- }
- title="Notifications"
- />
-
-
-
-
-
- } variant="contained">
- Send email
-
-
-
-
-
- columns={columns} rows={notifications} />
-
-
-
-
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_categories/payments.tsx b/002_source/cms/src/components/dashboard/lp_categories/payments.tsx
deleted file mode 100644
index 0420d32..0000000
--- a/002_source/cms/src/components/dashboard/lp_categories/payments.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import Avatar from '@mui/material/Avatar';
-import Box from '@mui/material/Box';
-import Button from '@mui/material/Button';
-import Card from '@mui/material/Card';
-import CardContent from '@mui/material/CardContent';
-import CardHeader from '@mui/material/CardHeader';
-import Chip from '@mui/material/Chip';
-import Divider from '@mui/material/Divider';
-import Link from '@mui/material/Link';
-import Stack from '@mui/material/Stack';
-import Typography from '@mui/material/Typography';
-import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
-import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple';
-
-import { dayjs } from '@/lib/dayjs';
-import type { ColumnDef } from '@/components/core/data-table';
-import { DataTable } from '@/components/core/data-table';
-
-export interface Payment {
- currency: string;
- amount: number;
- invoiceId: string;
- status: 'pending' | 'completed' | 'canceled' | 'refunded';
- createdAt: Date;
-}
-
-const columns = [
- {
- formatter: (row): React.JSX.Element => (
-
- {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)}
-
- ),
- name: 'Amount',
- width: '200px',
- },
- {
- formatter: (row): React.JSX.Element => {
- const mapping = {
- pending: { label: 'Pending', color: 'warning' },
- completed: { label: 'Completed', color: 'success' },
- canceled: { label: 'Canceled', color: 'error' },
- refunded: { label: 'Refunded', color: 'error' },
- } as const;
- const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' };
-
- return ;
- },
- name: 'Status',
- width: '200px',
- },
- {
- formatter: (row): React.JSX.Element => {
- return {row.invoiceId};
- },
- name: 'Invoice ID',
- width: '150px',
- },
- {
- formatter: (row): React.JSX.Element => (
-
- {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')}
-
- ),
- name: 'Date',
- align: 'right',
- },
-] satisfies ColumnDef[];
-
-export interface PaymentsProps {
- ordersValue: number;
- payments: Payment[];
- refundsValue: number;
- totalOrders: number;
-}
-
-export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element {
- return (
-
- }>
- Create Payment
-
- }
- avatar={
-
-
-
- }
- title="Payments"
- />
-
-
-
- }
- spacing={3}
- sx={{ justifyContent: 'space-between', p: 2 }}
- >
-
-
- Total orders
-
- {new Intl.NumberFormat('en-US').format(totalOrders)}
-
-
-
- Orders value
-
-
- {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)}
-
-
-
-
- Refunds
-
-
- {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)}
-
-
-
-
-
-
- columns={columns} rows={payments} />
-
-
-
-
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_categories/shipping-address.tsx b/002_source/cms/src/components/dashboard/lp_categories/shipping-address.tsx
deleted file mode 100644
index 8793e5c..0000000
--- a/002_source/cms/src/components/dashboard/lp_categories/shipping-address.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import * as React from 'react';
-import Button from '@mui/material/Button';
-import Card from '@mui/material/Card';
-import CardContent from '@mui/material/CardContent';
-import Chip from '@mui/material/Chip';
-import Stack from '@mui/material/Stack';
-import Typography from '@mui/material/Typography';
-import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
-
-export interface Address {
- id: string;
- country: string;
- state: string;
- city: string;
- zipCode: string;
- street: string;
- primary?: boolean;
-}
-
-export interface ShippingAddressProps {
- address: Address;
-}
-
-export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement {
- return (
-
-
-
-
- {address.street},
-
- {address.city}, {address.state}, {address.country},
-
- {address.zipCode}
-
-
- {address.primary ? : }
- }>
- Edit
-
-
-
-
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_categories/type.d.ts b/002_source/cms/src/components/dashboard/lp_categories/type.d.ts
deleted file mode 100644
index c66a11f..0000000
--- a/002_source/cms/src/components/dashboard/lp_categories/type.d.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-export interface LpCategory {
- isEmpty?: boolean;
- //
- id: string;
- collectionId: string;
- //
- cat_name: string;
- cat_image_url?: string;
- cat_image?: string;
- pos: number;
- visible: string;
- lesson_id: string;
- description: string;
- remarks: string;
- slug: string;
- init_answer: any;
- //
- name: string;
- avatar: string;
- email: string;
- phone: string;
- quota: number;
- status: 'pending' | 'active' | 'blocked' | 'NA';
- createdAt: Date;
-}
-
-export interface CreateFormProps {
- cat_name: string;
- cat_image: File[] | null;
- pos: number;
- init_answer?: string;
- visible: string;
- slug: string;
- remarks?: string;
- description?: string;
- //
- // TODO: to remove
- type: string;
- isActive: boolean;
- order: number;
- name?: string;
- imageUrl?: string;
-}
-
-export interface EditFormProps {
- cat_name: string;
- cat_image: File[] | null;
- pos: number;
- init_answer: any;
- visible: string;
- slug: string;
- remarks?: string;
- description?: string;
- //
- // TODO: remove below
- type: string;
-}
-
-export interface Helloworld {
- helloworld: string;
-}
diff --git a/002_source/cms/src/components/dashboard/lp_questions/_PROMPT.MD b/002_source/cms/src/components/dashboard/lp_questions/_PROMPT.MD
deleted file mode 100644
index 93ce5a6..0000000
--- a/002_source/cms/src/components/dashboard/lp_questions/_PROMPT.MD
+++ /dev/null
@@ -1 +0,0 @@
-please review and add translations, e.g. `{t('[word]')}`
diff --git a/002_source/cms/src/components/dashboard/lp_questions/_constants.ts b/002_source/cms/src/components/dashboard/lp_questions/_constants.ts
deleted file mode 100644
index dafb8fd..0000000
--- a/002_source/cms/src/components/dashboard/lp_questions/_constants.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { dayjs } from '@/lib/dayjs';
-
-import { CreateFormProps, LpQuestion } from './type';
-
-export const defaultLpQuestion: LpQuestion = {
- isEmpty: false,
- id: 'default-id',
- cat_name: 'default-category-name',
- cat_image_url: undefined,
- cat_image: undefined,
- pos: 0,
- visible: 'hidden',
- lesson_id: 'default-lesson-id',
- description: 'default-description',
- remarks: 'default-remarks',
- slug: '',
- init_answer: {},
- // from pocketbase
- collectionId: '0000000000',
- createdAt: dayjs('2099-01-01').toDate(),
- //
- name: '',
- avatar: '',
- email: '',
- phone: '',
- quota: 0,
- status: 'NA',
-};
-
-// export const LpCategoryCreateFormDefault: CreateFormProps = {
-// name: '',
-// type: '',
-// pos: 1,
-// visible: 'visible',
-// description: '',
-// isActive: true,
-// order: 1,
-// imageUrl: '',
-// };
-
-export const emptyLpCategory: LpQuestion = {
- ...defaultLpQuestion,
- isEmpty: true,
-};
diff --git a/002_source/cms/src/components/dashboard/lp_questions/confirm-delete-modal.tsx b/002_source/cms/src/components/dashboard/lp_questions/confirm-delete-modal.tsx
deleted file mode 100644
index e03ca52..0000000
--- a/002_source/cms/src/components/dashboard/lp_questions/confirm-delete-modal.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import deleteQuizLPQuestions from '@/db/QuizLPQuestions/Delete';
-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 { useTranslation } from 'react-i18next';
-
-import { logger } from '@/lib/default-logger';
-import { toast } from '@/components/core/toaster';
-
-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 handleUserConfirmDelete(): void {
- if (idToDelete) {
- setIsDeleteing(true);
- deleteQuizLPQuestions(idToDelete)
- .then(() => {
- reloadRows();
- handleClose();
- toast(t('delete.success'));
- })
- .catch((err) => {
- // console.error(err)
- logger.error(err);
- toast(t('delete.error'));
- })
- .finally(() => {
- setIsDeleteing(false);
- });
- }
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- {t('Delete Lesson Type ?')}
-
- {t('Are you sure you want to delete lesson type ?')}
-
-
-
-
- {
- handleUserConfirmDelete();
- }}
- loading={isDeleteing}
- >
- {t('Delete')}
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_questions/lp-question-create-form.tsx b/002_source/cms/src/components/dashboard/lp_questions/lp-question-create-form.tsx
deleted file mode 100644
index ddafd5b..0000000
--- a/002_source/cms/src/components/dashboard/lp_questions/lp-question-create-form.tsx
+++ /dev/null
@@ -1,419 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import RouterLink from 'next/link';
-import { useRouter } from 'next/navigation';
-import { COL_QUIZ_LP_QUESTIONS } from '@/constants';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { LoadingButton } from '@mui/lab';
-import { Avatar, Divider, MenuItem, Select } from '@mui/material';
-// import Avatar from '@mui/material/Avatar';
-import Box from '@mui/material/Box';
-import Button from '@mui/material/Button';
-import Card from '@mui/material/Card';
-import CardActions from '@mui/material/CardActions';
-import CardContent from '@mui/material/CardContent';
-import FormControl from '@mui/material/FormControl';
-import FormHelperText from '@mui/material/FormHelperText';
-import InputLabel from '@mui/material/InputLabel';
-import OutlinedInput from '@mui/material/OutlinedInput';
-import Stack from '@mui/material/Stack';
-import Typography from '@mui/material/Typography';
-import Grid from '@mui/material/Unstable_Grid2';
-import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
-// import axios from 'axios';
-import { Controller, useForm } from 'react-hook-form';
-import { useTranslation } from 'react-i18next';
-import { z as zod } from 'zod';
-
-import { paths } from '@/paths';
-import isDevelopment from '@/lib/check-is-development';
-import { logger } from '@/lib/default-logger';
-import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
-import { pb } from '@/lib/pb';
-import { TextEditor } from '@/components/core/text-editor/text-editor';
-import { toast } from '@/components/core/toaster';
-
-import type { CreateFormProps } from './type';
-
-const schema = zod.object({
- cat_name: zod.string().min(1, 'name-is-required').max(255),
- cat_image: zod.array(zod.any()).optional(),
- pos: zod.number().min(1, 'position is required').max(99),
- init_answer: zod.string().optional(),
- visible: zod.string(),
- slug: zod.string().min(1, 'slug-is-required').max(255),
- remarks: zod.string().optional(),
- description: zod.string().optional(),
- // NOTE: for image handling
- avatar: zod.string().optional(),
- // TODO: remove me
- type: zod.string().optional(),
- isActive: zod.boolean().optional(),
- order: zod.number().optional(),
-});
-
-type Values = zod.infer;
-
-export const defaultValues = {
- cat_name: '',
- cat_image: undefined,
- pos: 1,
- init_answer: '',
- visible: 'hidden',
- slug: '',
- remarks: '',
- description: '',
-} satisfies Values;
-
-export function LpQuestionCreateForm(): React.JSX.Element {
- const router = useRouter();
- const { t } = useTranslation(['lp_questions']);
-
- const [isCreating, setIsCreating] = React.useState(false);
-
- const {
- control,
- handleSubmit,
- formState: { errors },
- setValue,
- watch,
- } = useForm({ defaultValues, resolver: zodResolver(schema) });
-
- const onSubmit = React.useCallback(
- async (values: Values): Promise => {
- setIsCreating(true);
-
- const payload: CreateFormProps = {
- cat_name: values.cat_name,
- cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null,
- pos: values.pos,
- init_answer: values.init_answer,
- visible: values.visible,
- slug: values.slug,
- remarks: values.remarks,
- description: values.description,
- //
- // TODO: remove me
- type: 'type tet',
- isActive: true,
- order: 1,
- };
-
- try {
- const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).create(payload);
-
- logger.debug(result);
- toast.success(t('create.success'));
- router.push(paths.dashboard.lp_questions.list);
- } catch (error) {
- logger.error(error);
- toast.error(t('create.failed'));
- } finally {
- setIsCreating(false);
- }
- },
- // t is not necessary here
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [router]
- );
-
- const avatarInputRef = React.useRef(null);
- const avatar = watch('avatar');
-
- const handleAvatarChange = React.useCallback(
- async (event: React.ChangeEvent) => {
- const file = event.target.files?.[0];
-
- if (file) {
- const url = await fileToBase64(file);
- setValue('avatar', url);
- }
- },
- [setValue]
- );
-
- return (
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_questions/lp-question-edit-form.tsx b/002_source/cms/src/components/dashboard/lp_questions/lp-question-edit-form.tsx
deleted file mode 100644
index f4eb2ef..0000000
--- a/002_source/cms/src/components/dashboard/lp_questions/lp-question-edit-form.tsx
+++ /dev/null
@@ -1,500 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import RouterLink from 'next/link';
-import { useParams, useRouter } from 'next/navigation';
-//
-import { COL_QUIZ_LP_QUESTIONS } from '@/constants';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { LoadingButton } from '@mui/lab';
-//
-import Avatar from '@mui/material/Avatar';
-import Box from '@mui/material/Box';
-import Button from '@mui/material/Button';
-import Card from '@mui/material/Card';
-import CardActions from '@mui/material/CardActions';
-import CardContent from '@mui/material/CardContent';
-import Divider from '@mui/material/Divider';
-import FormControl from '@mui/material/FormControl';
-import FormHelperText from '@mui/material/FormHelperText';
-import InputLabel from '@mui/material/InputLabel';
-import MenuItem from '@mui/material/MenuItem';
-import OutlinedInput from '@mui/material/OutlinedInput';
-import Select from '@mui/material/Select';
-import Stack from '@mui/material/Stack';
-import Typography from '@mui/material/Typography';
-import Grid from '@mui/material/Unstable_Grid2';
-//
-import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
-//
-import { Controller, useForm } from 'react-hook-form';
-import { useTranslation } from 'react-i18next';
-import { z as zod } from 'zod';
-
-import { paths } from '@/paths';
-import { logger } from '@/lib/default-logger';
-import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
-import { pb } from '@/lib/pb';
-import { TextEditor } from '@/components/core/text-editor/text-editor';
-import { toast } from '@/components/core/toaster';
-import FormLoading from '@/components/loading';
-
-import ErrorDisplay from '../error';
-import type { EditFormProps } from './type';
-
-// TODO: review this
-const schema = zod.object({
- cat_name: zod.string().min(1, 'name-is-required').max(255),
- // accept file object when user change image
- // accept http string when user not changing image
- cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(),
-
- // position
- pos: zod.number().min(1, 'position is required').max(99),
-
- // it should be a valid JSON
- init_answer: zod
- .string()
- .refine(
- (value) => {
- try {
- JSON.parse(value);
- return true;
- } catch (error) {
- return false;
- }
- },
- { message: 'init_answer must be a valid JSON' }
- )
- .optional(),
- visible: zod.string(),
- slug: zod.string().min(0, 'slug-is-required').max(255).optional(),
- remarks: zod.string().optional(),
- description: zod.string().optional(),
- // NOTE: for image handling
- avatar: zod.string().optional(),
-});
-
-type Values = zod.infer;
-
-const defaultValues = {
- cat_name: '',
- cat_image: undefined,
- pos: 1,
- init_answer: JSON.stringify({}),
- visible: 'hidden',
- slug: '',
- remarks: '',
- description: '',
-} satisfies Values;
-
-export function LpQuestionEditForm(): React.JSX.Element {
- const router = useRouter();
- const { t } = useTranslation(['lp_questions']);
-
- const { cat_id: catId } = useParams<{ cat_id: string }>();
- //
- const [isUpdating, setIsUpdating] = React.useState(false);
- const [showLoading, setShowLoading] = React.useState(false);
- //
- const [showError, setShowError] = React.useState({ show: false, detail: '' });
-
- const {
- control,
- handleSubmit,
- formState: { errors },
- setValue,
- reset,
- watch,
- } = useForm({ defaultValues, resolver: zodResolver(schema) });
-
- const onSubmit = React.useCallback(
- async (values: Values): Promise => {
- setIsUpdating(true);
-
- const tempUpdate: EditFormProps = {
- cat_name: values.cat_name,
- cat_image: values.avatar ? [await base64ToFile(values.avatar)] : null,
- pos: values.pos,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- init_answer: JSON.parse(values.init_answer || '{}'),
-
- visible: values.visible,
- slug: values.slug || 'not-defined',
- remarks: values.remarks,
- description: values.description,
- //
- // TODO: remove below
- type: '',
- };
-
- try {
- const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).update(catId, tempUpdate);
- logger.debug(result);
- toast.success(t('edit.success'));
- router.push(paths.dashboard.lp_questions.list);
- } catch (error) {
- logger.error(error);
- toast.error(t('update.failed'));
- } finally {
- setIsUpdating(false);
- }
- },
- // t is not necessary here
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [router]
- );
-
- const avatarInputRef = React.useRef(null);
- const avatar = watch('avatar');
-
- const handleAvatarChange = React.useCallback(
- async (event: React.ChangeEvent) => {
- const file = event.target.files?.[0];
-
- if (file) {
- const url = await fileToBase64(file);
- setValue('avatar', url);
- }
- },
- [setValue]
- );
-
- // TODO: need to align with save form
- // use trycatch
- const [textDescription, setTextDescription] = React.useState('');
- const [textRemarks, setTextRemarks] = React.useState('');
-
- // load existing data when user arrive
- const loadExistingData = React.useCallback(
- async (id: string) => {
- setShowLoading(true);
-
- try {
- const result = await pb.collection(COL_QUIZ_LP_QUESTIONS).getOne(id);
-
- reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) });
- setTextDescription(result.description);
- setTextRemarks(result.remarks);
-
- if (result.cat_image !== '') {
- const fetchResult = await fetch(
- `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.cat_image}`
- );
-
- const blob = await fetchResult.blob();
- const url = await fileToBase64(blob);
-
- setValue('avatar', url);
- } else {
- setValue('avatar', '');
- }
- } catch (error) {
- logger.error(error);
- toast(t('list.error'));
-
- setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
- } finally {
- setShowLoading(false);
- }
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [catId]
- );
-
- React.useEffect(() => {
- void loadExistingData(catId);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [catId]);
-
- if (showLoading) return ;
- if (showError.show)
- return (
-
- );
-
- return (
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_questions/lp-questions-filters.tsx b/002_source/cms/src/components/dashboard/lp_questions/lp-questions-filters.tsx
deleted file mode 100644
index 36c4b0b..0000000
--- a/002_source/cms/src/components/dashboard/lp_questions/lp-questions-filters.tsx
+++ /dev/null
@@ -1,456 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import { useRouter } from 'next/navigation';
-// import { COL_LESSON_CATEGORIES } from '@/constants';
-import GetAllCount from '@/db/QuizListenings/GetAllCount';
-import GetHiddenCount from '@/db/QuizListenings/GetHiddenCount';
-import GetVisibleCount from '@/db/QuizListenings/GetVisibleCount';
-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 { useLpCategoriesSelection } from './lp-questions-selection-context';
-import { LpQuestion } from './type';
-
-export interface Filters {
- email?: string;
- phone?: string;
- status?: string;
- name?: string;
- visible?: string;
- type?: string;
-}
-
-export type SortDir = 'asc' | 'desc';
-
-export interface LpCategoriesFiltersProps {
- filters?: Filters;
- sortDir?: SortDir;
- fullData: LpQuestion[];
-}
-
-export function LpQuestionsFilters({
- filters = {},
- sortDir = 'desc',
- fullData,
-}: LpCategoriesFiltersProps): React.JSX.Element {
- const { t } = useTranslation();
- const { email, phone, status, name, visible, type } = filters;
-
- const [totalCount, setTotalCount] = React.useState(0);
- const [visibleCount, setVisibleCount] = React.useState(0);
- const [hiddenCount, setHiddenCount] = React.useState(0);
-
- const router = useRouter();
-
- const selection = useLpCategoriesSelection();
-
- function getVisible(): number {
- return fullData.reduce((count, item: LpQuestion) => {
- return item.visible === 'visible' ? count + 1 : count;
- }, 0);
- }
-
- function getHidden(): number {
- return fullData.reduce((count, item: LpQuestion) => {
- return item.visible === 'hidden' ? count + 1 : count;
- }, 0);
- }
-
- // The tabs should be generated using API data.
- const tabs = [
- { label: t('All'), value: '', count: totalCount },
- // { label: 'Active', value: 'active', count: 3 },
- // { label: 'Pending', value: 'pending', count: 1 },
- // { label: 'Blocked', value: 'blocked', count: 1 },
- { label: t('visible'), value: 'visible', count: visibleCount },
- { label: t('hidden'), value: 'hidden', count: hiddenCount },
- ] 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);
- }
-
- // NOTE: modify according to COLLECTION
- router.push(`${paths.dashboard.lp_questions.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]
- );
-
- React.useEffect(() => {
- const fetchCount = async (): Promise => {
- try {
- const tc = await GetAllCount();
- setTotalCount(tc);
-
- const vc = await GetVisibleCount();
- setVisibleCount(vc);
-
- const hc = await GetHiddenCount();
- setHiddenCount(hc);
- } catch (error) {
- //
- }
- };
- void fetchCount();
- }, []);
-
- 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}
- />
-
- {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('');
- const { t } = useTranslation();
-
- 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('');
- const { t } = useTranslation();
-
- 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/lp_questions/lp-questions-pagination.tsx b/002_source/cms/src/components/dashboard/lp_questions/lp-questions-pagination.tsx
deleted file mode 100644
index 652ed55..0000000
--- a/002_source/cms/src/components/dashboard/lp_questions/lp-questions-pagination.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import TablePagination from '@mui/material/TablePagination';
-
-function noop(): void {
- return undefined;
-}
-
-interface LessonCategoriesPaginationProps {
- count: number;
- page: number;
- //
- setPage: (page: number) => void;
- setRowsPerPage: (page: number) => void;
- rowsPerPage: number;
-}
-
-export function LpQuestionsPagination({
- count,
- page,
- //
- setPage,
- setRowsPerPage,
- rowsPerPage,
-}: 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.
- const handleChangePage = (event: unknown, newPage: number) => {
- setPage(newPage);
- };
-
- const handleChangeRowsPerPage = (event: React.ChangeEvent) => {
- setRowsPerPage(parseInt(event.target.value));
- // console.log(parseInt(event.target.value));
- };
-
- return (
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_questions/lp-questions-selection-context.tsx b/002_source/cms/src/components/dashboard/lp_questions/lp-questions-selection-context.tsx
deleted file mode 100644
index b4fafda..0000000
--- a/002_source/cms/src/components/dashboard/lp_questions/lp-questions-selection-context.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-'use client';
-
-import * as React from 'react';
-
-// import type { LessonCategory } from '@/types/lesson-type';
-import { useSelection } from '@/hooks/use-selection';
-import type { Selection } from '@/hooks/use-selection';
-
-import { LpQuestion } from './type';
-
-function noop(): void {
- return undefined;
-}
-
-export interface LpCategoriesSelectionContextValue extends Selection {}
-
-export const LpCategoriesSelectionContext = React.createContext({
- deselectAll: noop,
- deselectOne: noop,
- selectAll: noop,
- selectOne: noop,
- selected: new Set(),
- selectedAny: false,
- selectedAll: false,
-});
-
-interface LpCategoriesSelectionProviderProps {
- children: React.ReactNode;
- lessonQuestions: LpQuestion[];
-}
-
-export function LpQuestionsSelectionProvider({
- children,
- lessonQuestions: lessonCategories = [],
-}: LpCategoriesSelectionProviderProps): React.JSX.Element {
- const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]);
- const selection = useSelection(customerIds);
-
- return (
- {children}
- );
-}
-
-export function useLpCategoriesSelection(): LpCategoriesSelectionContextValue {
- return React.useContext(LpCategoriesSelectionContext);
-}
diff --git a/002_source/cms/src/components/dashboard/lp_questions/lp-questions-table.tsx b/002_source/cms/src/components/dashboard/lp_questions/lp-questions-table.tsx
deleted file mode 100644
index eab1353..0000000
--- a/002_source/cms/src/components/dashboard/lp_questions/lp-questions-table.tsx
+++ /dev/null
@@ -1,241 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import RouterLink from 'next/link';
-import { LoadingButton } from '@mui/lab';
-import Avatar from '@mui/material/Avatar';
-import Box from '@mui/material/Box';
-import Button from '@mui/material/Button';
-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 { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images';
-import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus';
-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 { dayjs } from '@/lib/dayjs';
-import { DataTable } from '@/components/core/data-table';
-import type { ColumnDef } from '@/components/core/data-table';
-
-import ConfirmDeleteModal from './confirm-delete-modal';
-import { useLpCategoriesSelection } from './lp-questions-selection-context';
-import type { LpQuestion } from './type';
-
-function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] {
- return [
- {
- formatter: (row): React.JSX.Element => (
-
-
-
-
-
- {' '}
-
- {row.cat_name}
-
- slug: {row.cat_name}
-
-
-
-
-
- ),
- name: 'Name',
- width: '200px',
- },
- {
- formatter: (row): React.JSX.Element => (
-
-
-
- {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)}
-
-
- ),
- // NOTE: please refer to translation.json here
- name: 'word-count',
- width: '100px',
- },
- {
- formatter: (row): React.JSX.Element => {
- // eslint-disable-next-line react-hooks/rules-of-hooks
-
- const mapping = {
- active: {
- label: 'Active',
- icon: (
-
- ),
- },
- blocked: { label: 'Blocked', icon: },
- pending: {
- label: 'Pending',
- icon: (
-
- ),
- },
- NA: {
- label: 'NA',
- icon: (
-
- ),
- },
- } as const;
- const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
-
- return (
-
- );
- },
- name: 'visible',
- width: '150px',
- },
- {
- formatter(row) {
- return dayjs(row.createdAt).format('MMM D, YYYY');
- },
- name: 'Created at',
- width: '100px',
- },
- {
- formatter: (row): React.JSX.Element => (
-
-
-
-
- {
- handleDeleteClick(row.id);
- }}
- >
-
-
-
- ),
- name: 'Actions',
- hideName: true,
- width: '100px',
- align: 'right',
- },
- ];
-}
-
-export interface LessonCategoriesTableProps {
- rows: LpQuestion[];
- reloadRows: () => void;
-}
-
-export function LpQuestionsTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element {
- const { t } = useTranslation(['lp_questions']);
- const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpCategoriesSelection();
-
- const [idToDelete, setIdToDelete] = React.useState('');
- const [open, setOpen] = React.useState(false);
-
- function handleDeleteClick(testId: string): void {
- setOpen(true);
- setIdToDelete(testId);
- }
-
- return (
-
-
-
- columns={columns(handleDeleteClick)}
- onDeselectAll={deselectAll}
- onDeselectOne={(_, row) => {
- deselectOne(row.id);
- }}
- onSelectAll={selectAll}
- onSelectOne={(_, row) => {
- selectOne(row.id);
- }}
- rows={rows}
- selectable
- selected={selected}
- />
- {!rows.length ? (
-
-
- {t('no-lesson-categories-found')}
-
-
- ) : null}
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_questions/notifications.tsx b/002_source/cms/src/components/dashboard/lp_questions/notifications.tsx
deleted file mode 100644
index a6c16bd..0000000
--- a/002_source/cms/src/components/dashboard/lp_questions/notifications.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import Avatar from '@mui/material/Avatar';
-import Box from '@mui/material/Box';
-import Button from '@mui/material/Button';
-import Card from '@mui/material/Card';
-import CardContent from '@mui/material/CardContent';
-import CardHeader from '@mui/material/CardHeader';
-import Chip from '@mui/material/Chip';
-import Select from '@mui/material/Select';
-import Stack from '@mui/material/Stack';
-import Typography from '@mui/material/Typography';
-import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple';
-
-import { dayjs } from '@/lib/dayjs';
-import { DataTable } from '@/components/core/data-table';
-import type { ColumnDef } from '@/components/core/data-table';
-import { Option } from '@/components/core/option';
-
-export interface Notification {
- id: string;
- type: string;
- status: 'delivered' | 'pending' | 'failed';
- createdAt: Date;
-}
-
-const columns = [
- {
- formatter: (row): React.JSX.Element => (
-
- {row.type}
-
- ),
- name: 'Type',
- width: '300px',
- },
- {
- formatter: (row): React.JSX.Element => {
- const mapping = {
- delivered: { label: 'Delivered', color: 'success' },
- pending: { label: 'Pending', color: 'warning' },
- failed: { label: 'Failed', color: 'error' },
- } as const;
- const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' };
-
- return ;
- },
- name: 'Status',
- width: '200px',
- },
- {
- formatter: (row): React.JSX.Element => (
-
- {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')}
-
- ),
- name: 'Date',
- align: 'right',
- },
-] satisfies ColumnDef[];
-
-export interface NotificationsProps {
- notifications: Notification[];
-}
-
-export function Notifications({ notifications }: NotificationsProps): React.JSX.Element {
- return (
-
-
-
-
- }
- title="Notifications"
- />
-
-
-
-
-
- } variant="contained">
- Send email
-
-
-
-
-
- columns={columns} rows={notifications} />
-
-
-
-
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_questions/payments.tsx b/002_source/cms/src/components/dashboard/lp_questions/payments.tsx
deleted file mode 100644
index 0420d32..0000000
--- a/002_source/cms/src/components/dashboard/lp_questions/payments.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import Avatar from '@mui/material/Avatar';
-import Box from '@mui/material/Box';
-import Button from '@mui/material/Button';
-import Card from '@mui/material/Card';
-import CardContent from '@mui/material/CardContent';
-import CardHeader from '@mui/material/CardHeader';
-import Chip from '@mui/material/Chip';
-import Divider from '@mui/material/Divider';
-import Link from '@mui/material/Link';
-import Stack from '@mui/material/Stack';
-import Typography from '@mui/material/Typography';
-import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
-import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple';
-
-import { dayjs } from '@/lib/dayjs';
-import type { ColumnDef } from '@/components/core/data-table';
-import { DataTable } from '@/components/core/data-table';
-
-export interface Payment {
- currency: string;
- amount: number;
- invoiceId: string;
- status: 'pending' | 'completed' | 'canceled' | 'refunded';
- createdAt: Date;
-}
-
-const columns = [
- {
- formatter: (row): React.JSX.Element => (
-
- {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)}
-
- ),
- name: 'Amount',
- width: '200px',
- },
- {
- formatter: (row): React.JSX.Element => {
- const mapping = {
- pending: { label: 'Pending', color: 'warning' },
- completed: { label: 'Completed', color: 'success' },
- canceled: { label: 'Canceled', color: 'error' },
- refunded: { label: 'Refunded', color: 'error' },
- } as const;
- const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' };
-
- return ;
- },
- name: 'Status',
- width: '200px',
- },
- {
- formatter: (row): React.JSX.Element => {
- return {row.invoiceId};
- },
- name: 'Invoice ID',
- width: '150px',
- },
- {
- formatter: (row): React.JSX.Element => (
-
- {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')}
-
- ),
- name: 'Date',
- align: 'right',
- },
-] satisfies ColumnDef[];
-
-export interface PaymentsProps {
- ordersValue: number;
- payments: Payment[];
- refundsValue: number;
- totalOrders: number;
-}
-
-export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element {
- return (
-
- }>
- Create Payment
-
- }
- avatar={
-
-
-
- }
- title="Payments"
- />
-
-
-
- }
- spacing={3}
- sx={{ justifyContent: 'space-between', p: 2 }}
- >
-
-
- Total orders
-
- {new Intl.NumberFormat('en-US').format(totalOrders)}
-
-
-
- Orders value
-
-
- {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)}
-
-
-
-
- Refunds
-
-
- {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)}
-
-
-
-
-
-
- columns={columns} rows={payments} />
-
-
-
-
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_questions/shipping-address.tsx b/002_source/cms/src/components/dashboard/lp_questions/shipping-address.tsx
deleted file mode 100644
index 8793e5c..0000000
--- a/002_source/cms/src/components/dashboard/lp_questions/shipping-address.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import * as React from 'react';
-import Button from '@mui/material/Button';
-import Card from '@mui/material/Card';
-import CardContent from '@mui/material/CardContent';
-import Chip from '@mui/material/Chip';
-import Stack from '@mui/material/Stack';
-import Typography from '@mui/material/Typography';
-import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
-
-export interface Address {
- id: string;
- country: string;
- state: string;
- city: string;
- zipCode: string;
- street: string;
- primary?: boolean;
-}
-
-export interface ShippingAddressProps {
- address: Address;
-}
-
-export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement {
- return (
-
-
-
-
- {address.street},
-
- {address.city}, {address.state}, {address.country},
-
- {address.zipCode}
-
-
- {address.primary ? : }
- }>
- Edit
-
-
-
-
-
- );
-}
diff --git a/002_source/cms/src/components/dashboard/lp_questions/type.d.ts b/002_source/cms/src/components/dashboard/lp_questions/type.d.ts
deleted file mode 100644
index 8975fb4..0000000
--- a/002_source/cms/src/components/dashboard/lp_questions/type.d.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-export interface LpQuestion {
- isEmpty?: boolean;
- //
- id: string;
- collectionId: string;
- //
- cat_name: string;
- cat_image_url?: string;
- cat_image?: string;
- pos: number;
- visible: string;
- lesson_id: string;
- description: string;
- remarks: string;
- slug: string;
- init_answer: any;
- //
- name: string;
- avatar: string;
- email: string;
- phone: string;
- quota: number;
- status: 'pending' | 'active' | 'blocked' | 'NA';
- createdAt: Date;
-}
-
-export interface CreateFormProps {
- cat_name: string;
- cat_image: File[] | null;
- pos: number;
- init_answer?: string;
- visible: string;
- slug: string;
- remarks?: string;
- description?: string;
- //
- // TODO: to remove
- type: string;
- isActive: boolean;
- order: number;
- name?: string;
- imageUrl?: string;
-}
-
-export interface EditFormProps {
- cat_name: string;
- cat_image: File[] | null;
- pos: number;
- init_answer: any;
- visible: string;
- slug: string;
- remarks?: string;
- description?: string;
- //
- // TODO: remove below
- type: string;
-}
-
-export interface Helloworld {
- helloworld: string;
-}