diff --git a/002_source/cms/public/locales/dev/translation.json b/002_source/cms/public/locales/dev/translation.json index 75986b8..d298f0b 100644 --- a/002_source/cms/public/locales/dev/translation.json +++ b/002_source/cms/public/locales/dev/translation.json @@ -166,12 +166,10 @@ "Delete": "刪除", "All": "全部", "categories": "問題分類", - "listening-practice": "聽講練習", - "matching-frenzy": "配對練習", - "connective-revision": "連接詞練習", + "listening-practice": "聽講練習 (LP)", + "matching-frenzy": "配對練習 (MF)", + "connective-revision": "連接詞練習 (CR)", "teachers": "導師", - "students": "學生", - "word-count": "字數", "questions": "題目", "loading": "載入中", "Notifications": "通知", diff --git a/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/BasicDetailCard.tsx b/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/BasicDetailCard.tsx index bf02e80..b2dfbef 100644 --- a/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/BasicDetailCard.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/BasicDetailCard.tsx @@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next'; import { PropertyItem } from '@/components/core/property-item'; import { PropertyList } from '@/components/core/property-list'; -import { LpQuestion } from '@/components/dashboard/lp_questions.plan/type'; +import { LpQuestion } from '@/components/dashboard/lp_questions_del/type'; export default function BasicDetailCard({ lpModel: model, diff --git a/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/TitleCard.tsx b/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/TitleCard.tsx index 67deaac..19b1b53 100644 --- a/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/TitleCard.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/TitleCard.tsx @@ -10,7 +10,7 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; import { useTranslation } from 'react-i18next'; -import { LpQuestion } from '@/components/dashboard/lp_questions.plan/type'; +import { LpQuestion } from '@/components/dashboard/lp_questions_del/type'; function getImageUrlFrRecord(record: LpQuestion): string { return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.question_image}`; diff --git a/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/page.tsx index f22c443..d76ffa8 100644 --- a/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions.plan/[cat_id]/page.tsx @@ -21,9 +21,9 @@ import { logger } from '@/lib/default-logger'; import { pb } from '@/lib/pb'; import { toast } from '@/components/core/toaster'; import ErrorDisplay from '@/components/dashboard/error'; -import { defaultLpQuestion } from '@/components/dashboard/lp_questions.plan/_constants'; -import { Notifications } from '@/components/dashboard/lp_questions.plan/notifications'; -import type { LpQuestion } from '@/components/dashboard/lp_questions.plan/type'; +import { defaultLpQuestion } from '@/components/dashboard/lp_questions_del/_constants'; +import { Notifications } from '@/components/dashboard/lp_questions_del/notifications'; +import type { LpQuestion } from '@/components/dashboard/lp_questions_del/type'; import FormLoading from '@/components/loading'; import BasicDetailCard from './BasicDetailCard'; diff --git a/002_source/cms/src/app/dashboard/lp/questions.plan/create/page.tsx b/002_source/cms/src/app/dashboard/lp/questions.plan/create/page.tsx index f83acf2..a6d48e2 100644 --- a/002_source/cms/src/app/dashboard/lp/questions.plan/create/page.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions.plan/create/page.tsx @@ -13,7 +13,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; -import { LpQuestionCreateForm } from '@/components/dashboard/lp_questions.plan/lp-question-create-form'; +import { LpQuestionCreateForm } from '@/components/dashboard/lp_questions_del/lp-question-create-form'; export default function Page(): React.JSX.Element { // RULES: follow the name of page directory diff --git a/002_source/cms/src/app/dashboard/lp/questions.plan/edit/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/lp/questions.plan/edit/[cat_id]/page.tsx index 45a9b7a..bfd52f5 100644 --- a/002_source/cms/src/app/dashboard/lp/questions.plan/edit/[cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions.plan/edit/[cat_id]/page.tsx @@ -10,7 +10,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow import { useTranslation } from 'react-i18next'; import { paths } from '@/paths'; -import { LpQuestionEditForm } from '@/components/dashboard/lp_questions.plan/lp-question-edit-form'; +import { LpQuestionEditForm } from '@/components/dashboard/lp_questions_del/lp-question-edit-form'; export default function Page(): React.JSX.Element { const { t } = useTranslation(['lp_questions']); diff --git a/002_source/cms/src/app/dashboard/lp/questions.plan/page.tsx b/002_source/cms/src/app/dashboard/lp/questions.plan/page.tsx index edf1f1d..6fa53ab 100644 --- a/002_source/cms/src/app/dashboard/lp/questions.plan/page.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions.plan/page.tsx @@ -22,13 +22,13 @@ import { logger } from '@/lib/default-logger'; import { pb } from '@/lib/pb'; import { toast } from '@/components/core/toaster'; import ErrorDisplay from '@/components/dashboard/error'; -import { defaultLpQuestion } from '@/components/dashboard/lp_questions.plan/_constants'; -import { LpQuestionsFilters } from '@/components/dashboard/lp_questions.plan/lp-questions-filters'; -import type { Filters } from '@/components/dashboard/lp_questions.plan/lp-questions-filters'; -import { LpQuestionsPagination } from '@/components/dashboard/lp_questions.plan/lp-questions-pagination'; -import { LpQuestionsSelectionProvider } from '@/components/dashboard/lp_questions.plan/lp-questions-selection-context'; -import { LpQuestionsTable } from '@/components/dashboard/lp_questions.plan/lp-questions-table'; -import type { LpQuestion } from '@/components/dashboard/lp_questions.plan/type'; +import { defaultLpQuestion } from '@/components/dashboard/lp_questions_del/_constants'; +import { LpQuestionsFilters } from '@/components/dashboard/lp_questions_del/lp-questions-filters'; +import type { Filters } from '@/components/dashboard/lp_questions_del/lp-questions-filters'; +import { LpQuestionsPagination } from '@/components/dashboard/lp_questions_del/lp-questions-pagination'; +import { LpQuestionsSelectionProvider } from '@/components/dashboard/lp_questions_del/lp-questions-selection-context'; +import { LpQuestionsTable } from '@/components/dashboard/lp_questions_del/lp-questions-table'; +import type { LpQuestion } from '@/components/dashboard/lp_questions_del/type'; import FormLoading from '@/components/loading'; export default function Page({ searchParams }: PageProps): React.JSX.Element { diff --git a/002_source/cms/src/app/dashboard/lp/questions/page.tsx b/002_source/cms/src/app/dashboard/lp/questions/page.tsx index ca2c0b6..5640ec4 100644 --- a/002_source/cms/src/app/dashboard/lp/questions/page.tsx +++ b/002_source/cms/src/app/dashboard/lp/questions/page.tsx @@ -73,7 +73,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { setShowError({ // show: true, - code: error.status, detail: JSON.stringify(error, null, 2), }); } finally { @@ -141,7 +140,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { 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 index ba4020d..c00e964 100644 --- 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 @@ -39,7 +39,7 @@ 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 ErrorDisplay from '../../error'; import type { EditFormProps } from './type'; // TODO: review this diff --git a/002_source/cms/src/components/dashboard/lp/questions/_constants.ts b/002_source/cms/src/components/dashboard/lp/questions/_constants.ts index 2173368..95a4d17 100644 --- a/002_source/cms/src/components/dashboard/lp/questions/_constants.ts +++ b/002_source/cms/src/components/dashboard/lp/questions/_constants.ts @@ -5,7 +5,7 @@ import { CreateFormProps, LpQuestion } from './type'; export const defaultLpQuestion: LpQuestion = { isEmpty: false, id: 'default-id', - q_name: 'default-question-name', + cat_name: 'default-question-name', cat_image_url: undefined, cat_image: undefined, pos: 0, 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 index 40aca33..a39ad69 100644 --- 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 @@ -37,8 +37,8 @@ import { toast } from '@/components/core/toaster'; import type { CreateFormProps } from './type'; const schema = zod.object({ - q_name: zod.string().min(1, 'name-is-required').max(255), - q_image: zod.array(zod.any()).optional(), + 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(), 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 index 0f715c1..a38d9b2 100644 --- 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 @@ -39,15 +39,15 @@ 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 ErrorDisplay from '../../error'; import type { EditFormProps } from './type'; // TODO: review this const schema = zod.object({ - q_name: zod.string().min(1, 'name-is-required').max(255), + 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 - q_image: zod.union([zod.array(zod.any()), zod.string()]).optional(), + cat_image: zod.union([zod.array(zod.any()), zod.string()]).optional(), // position pos: zod.number().min(1, 'position is required').max(99), 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 index 5334248..deef393 100644 --- 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 @@ -23,7 +23,7 @@ export function LpQuestionsPagination({ setPage, setRowsPerPage, rowsPerPage, -}: LessonCategoriesPaginationProps): React.JSX.Element { +}: LessonQuestionsPaginationProps): 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) => { 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 index fe47149..2cde8d7 100644 --- 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 @@ -6,7 +6,7 @@ import * as React from 'react'; import { useSelection } from '@/hooks/use-selection'; import type { Selection } from '@/hooks/use-selection'; -import { LpQuestion } from './type'; +import type { LpQuestion } from './type'; function noop(): void { return undefined; @@ -32,7 +32,7 @@ interface LpQuestionsSelectionProviderProps { export function LpQuestionsSelectionProvider({ children, lessonCategories = [], -}: LpCategoriesSelectionProviderProps): React.JSX.Element { +}: LpQuestionsSelectionProviderProps): React.JSX.Element { const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]); const selection = useSelection(customerIds); @@ -42,5 +42,5 @@ export function LpQuestionsSelectionProvider({ } export function useLpQuestionsSelection(): LpQuestionsSelectionContextValue { - return React.useContext(LpCategoriesSelectionContext); + return React.useContext(LpQuestionsSelectionContext); } 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 index 8369423..e78356c 100644 --- 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 @@ -193,7 +193,7 @@ export interface LessonQuestionsTableProps { export function LpQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps): React.JSX.Element { const { t } = useTranslation(['lp_categories']); - const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpCategoriesSelection(); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpQuestionsSelection(); const [idToDelete, setIdToDelete] = React.useState(''); const [open, setOpen] = React.useState(false); @@ -211,7 +211,7 @@ export function LpQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps reloadRows={reloadRows} setOpen={setOpen} /> - + columns={columns(handleDeleteClick)} onDeselectAll={deselectAll} onDeselectOne={(_, row) => { 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 index 3e6e084..8975fb4 100644 --- a/002_source/cms/src/components/dashboard/lp/questions/type.d.ts +++ b/002_source/cms/src/components/dashboard/lp/questions/type.d.ts @@ -4,9 +4,9 @@ export interface LpQuestion { id: string; collectionId: string; // - q_name: string; - q_image_url?: string; - q_image?: string; + cat_name: string; + cat_image_url?: string; + cat_image?: string; pos: number; visible: string; lesson_id: string; @@ -25,8 +25,8 @@ export interface LpQuestion { } export interface CreateFormProps { - q_name: string; - q_image: File[] | null; + cat_name: string; + cat_image: File[] | null; pos: number; init_answer?: string; visible: string; @@ -43,8 +43,8 @@ export interface CreateFormProps { } export interface EditFormProps { - q_name: string; - q_image: File[] | null; + cat_name: string; + cat_image: File[] | null; pos: number; init_answer: any; visible: string; diff --git a/002_source/cms/src/components/dashboard/lp_questions.plan/_PROMPT.MD b/002_source/cms/src/components/dashboard/lp_questions.plan/_PROMPT.MD deleted file mode 100644 index 93ce5a6..0000000 --- a/002_source/cms/src/components/dashboard/lp_questions.plan/_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.plan/_constants.ts b/002_source/cms/src/components/dashboard/lp_questions.plan/_constants.ts deleted file mode 100644 index 885067e..0000000 --- a/002_source/cms/src/components/dashboard/lp_questions.plan/_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_questions.plan/confirm-delete-modal.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/confirm-delete-modal.tsx deleted file mode 100644 index 32daa09..0000000 --- a/002_source/cms/src/components/dashboard/lp_questions.plan/confirm-delete-modal.tsx +++ /dev/null @@ -1,122 +0,0 @@ -'use client'; - -import * as React from 'react'; -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 { 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_questions.plan/lp-question-create-form.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-question-create-form.tsx deleted file mode 100644 index d7548cc..0000000 --- a/002_source/cms/src/components/dashboard/lp_questions.plan/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({ - question_name: zod.string().min(1, 'name-is-required').max(255), - question_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 = { - question_name: '', - question_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 = { - question_name: values.question_name, - question_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 ( -
- - - } - spacing={4} - > - - {t('create.basic-info')} - - - - - - - - - - {t('create.avatar')} - {t('create.avatarRequirements')} - - - - - - - ( - - {t('create.question_name')} - - {errors.question_name ? {errors.question_name.message} : null} - - )} - /> - - {/* */} - - ( - - {t('create.pos')} - { - field.onChange(parseInt(e.target.value)); - }} - type="number" - /> - {errors.pos ? {errors.pos.message} : null} - - )} - /> - - {/* */} - - ( - - {t('create.slug')} - - {errors.slug ? {errors.slug.message} : null} - - )} - /> - - {/* */} - - ( - - {t('edit.visible')} - - - {errors.visible ? {errors.visible.message} : null} - - )} - /> - - {/* */} - - ( - - {t('create.init_answer')} - - {errors.init_answer ? {errors.init_answer.message} : null} - - )} - /> - - - - {/* */} - - {t('create.detail-information')} - - - { - return ( - - - {t('create.description')} - - - { - field.onChange({ target: { value: editor.getHTML() } }); - }} - placeholder={t('create.description.default')} - /> - - - ); - }} - /> - - - ( - - - {t('create.remarks')} - - - { - field.onChange({ target: { value: editor.getText() } }); - }} - hideToolbar - placeholder={t('create.remarks.default')} - /> - - - )} - /> - - - - {/* */} - - - - - - - {t('create.createButton')} - - - - -
{JSON.stringify({ errors }, null, 2)}
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/lp_questions.plan/lp-question-edit-form.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-question-edit-form.tsx deleted file mode 100644 index 61f7aec..0000000 --- a/002_source/cms/src/components/dashboard/lp_questions.plan/lp-question-edit-form.tsx +++ /dev/null @@ -1,499 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -// -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({ - question_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 - question_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 = { - question_name: '', - question_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 = { - question_name: values.question_name, - question_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_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_QUESTIONS).getOne(id); - - reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) }); - setTextDescription(result.description); - setTextRemarks(result.remarks); - - if (result.question_image !== '') { - const fetchResult = await fetch( - `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.question_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 ( -
- - - } - spacing={4} - > - - {t('edit.basic-info')} - - - - - - - - - - {t('edit.avatar')} - {t('edit.avatarRequirements')} - - - - - - - ( - - {t('edit.question_name')} - - {errors.question_name ? {errors.question_name.message} : null} - - )} - /> - - {/* */} - - ( - - {t('edit.pos')} - { - field.onChange(parseInt(e.target.value)); - }} - type="number" - /> - {errors.pos ? {errors.pos.message} : null} - - )} - /> - - {/* */} - - ( - - {t('edit.slug')} - - {errors.slug ? {errors.slug.message} : null} - - )} - /> - - {/* */} - - ( - - {t('edit.visible')} - - - {errors.visible ? {errors.visible.message} : null} - - )} - /> - - {/* */} - - ( - - {t('edit.init_answer')} - - {errors.init_answer ? {errors.init_answer.message} : null} - - )} - /> - - - - {/* */} - - {t('edit.detail-information')} - - - { - return ( - - - {t('edit.description')} - - - { - field.onChange({ target: { value: editor.getHTML() } }); - }} - placeholder={t('edit.description.default')} - /> - - - ); - }} - /> - - - ( - - - {t('edit.remarks')} - - - { - field.onChange({ target: { value: editor.getText() } }); - }} - hideToolbar - placeholder={t('edit.remarks.default')} - /> - - - )} - /> - - - - {/* */} - - - - - - - {t('edit.updateButton')} - - - -
- ); -} diff --git a/002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-filters.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-filters.tsx deleted file mode 100644 index 168ef12..0000000 --- a/002_source/cms/src/components/dashboard/lp_questions.plan/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 { 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_questions.plan/lp-questions-pagination.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-pagination.tsx deleted file mode 100644 index bf40481..0000000 --- a/002_source/cms/src/components/dashboard/lp_questions.plan/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 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_questions.plan/lp-questions-selection-context.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-selection-context.tsx deleted file mode 100644 index a6c6502..0000000 --- a/002_source/cms/src/components/dashboard/lp_questions.plan/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 { 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_questions.plan/lp-questions-table.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/lp-questions-table.tsx deleted file mode 100644 index 5ef6506..0000000 --- a/002_source/cms/src/components/dashboard/lp_questions.plan/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 { 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_questions.plan/notifications.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/notifications.tsx deleted file mode 100644 index a6c16bd..0000000 --- a/002_source/cms/src/components/dashboard/lp_questions.plan/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" - /> - - - - -
- -
-
- - - columns={columns} rows={notifications} /> - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/lp_questions.plan/payments.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/payments.tsx deleted file mode 100644 index 0420d32..0000000 --- a/002_source/cms/src/components/dashboard/lp_questions.plan/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.plan/shipping-address.tsx b/002_source/cms/src/components/dashboard/lp_questions.plan/shipping-address.tsx deleted file mode 100644 index 8793e5c..0000000 --- a/002_source/cms/src/components/dashboard/lp_questions.plan/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 ? : } - - -
-
-
- ); -} diff --git a/002_source/cms/src/components/dashboard/lp_questions.plan/type.d.ts b/002_source/cms/src/components/dashboard/lp_questions.plan/type.d.ts deleted file mode 100644 index 1b9fc87..0000000 --- a/002_source/cms/src/components/dashboard/lp_questions.plan/type.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -export interface LpQuestion { - isEmpty?: boolean; - // - id: string; - collectionId: string; - // - question_name: string; - question_image_url?: string; - question_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 { - question_name: string; - question_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 { - question_name: string; - question_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/overview/summary/ActiveUserCount/index.tsx b/002_source/cms/src/components/dashboard/overview/summary/ActiveUserCount/index.tsx index 8e4da17..520ba73 100644 --- a/002_source/cms/src/components/dashboard/overview/summary/ActiveUserCount/index.tsx +++ b/002_source/cms/src/components/dashboard/overview/summary/ActiveUserCount/index.tsx @@ -29,7 +29,7 @@ function ActiveUserCount(): React.JSX.Element { const tempCount = await getAllUserMetasCount(); setAmount(tempCount); } catch (error) { - setAmount(-9); + setAmount(-99); setErrorDetail(JSON.stringify(error)); setShowError(true); } finally { @@ -40,7 +40,14 @@ function ActiveUserCount(): React.JSX.Element { }, []); if (showLoading) { - return ; + return ( + + ); } if (showError) return
{errorDetail}
; diff --git a/002_source/cms/src/db/QuizLPQuestions/Create.tsx b/002_source/cms/src/db/QuizLPQuestions/Create.tsx index a73c53d..4b74ff1 100644 --- a/002_source/cms/src/db/QuizLPQuestions/Create.tsx +++ b/002_source/cms/src/db/QuizLPQuestions/Create.tsx @@ -2,11 +2,12 @@ import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; import type { RecordModel } from 'pocketbase'; import { pb } from '@/lib/pb'; +import { CreateFormProps } from '@/components/dashboard/lp_questions/type'; -interface CreateForm { - // TODO: Add QuizLPQuestions fields -} +// interface CreateForm { +// // TODO: Add QuizLPQuestions fields +// } -export default function createQuizLPQuestion(data: CreateForm): Promise { +export default function createQuizLPQuestion(data: CreateFormProps): Promise { return pb.collection(COL_QUIZ_LP_QUESTIONS).create(data); } diff --git a/002_source/cms/src/db/QuizMFCategories/Create.tsx b/002_source/cms/src/db/QuizMFCategories/Create.tsx index 14276fb..25f8f1d 100644 --- a/002_source/cms/src/db/QuizMFCategories/Create.tsx +++ b/002_source/cms/src/db/QuizMFCategories/Create.tsx @@ -2,11 +2,8 @@ import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; import type { RecordModel } from 'pocketbase'; import { pb } from '@/lib/pb'; +import type { CreateFormProps } from '@/components/dashboard/mf_categories/type'; -interface CreateForm { - // TODO: Add QuizMFCategories fields -} - -export default function createQuizMFCategory(data: CreateForm): Promise { +export default function createQuizMFCategory(data: CreateFormProps): Promise { return pb.collection(COL_QUIZ_MF_CATEGORIES).create(data); } diff --git a/002_source/cms/src/paths.ts b/002_source/cms/src/paths.ts index d8976c1..008f01d 100644 --- a/002_source/cms/src/paths.ts +++ b/002_source/cms/src/paths.ts @@ -101,16 +101,16 @@ export const paths = { edit: (id: string) => `/dashboard/lp/questions/edit/${id}`, }, mf_categories: { - list: '/dashboard/mf_categories', - create: '/dashboard/mf_categories/create', - details: (id: string) => `/dashboard/mf_categories/${id}`, - edit: (id: string) => `/dashboard/mf_categories/edit/${id}`, + list: '/dashboard/mf/categories', + create: '/dashboard/mf/categories/create', + details: (id: string) => `/dashboard/mf/categories/${id}`, + edit: (id: string) => `/dashboard/mf/categories/edit/${id}`, }, mf_questions: { - list: '/dashboard/mf_questions', - create: '/dashboard/mf_questions/create', - details: (id: string) => `/dashboard/mf_questions/${id}`, - edit: (id: string) => `/dashboard/mf_questions/edit/${id}`, + list: '/dashboard/mf/questions', + create: '/dashboard/mf/questions/create', + details: (id: string) => `/dashboard/mf/questions/${id}`, + edit: (id: string) => `/dashboard/mf/questions/edit/${id}`, }, cr_categories: { list: '/dashboard/cr_categories',