diff --git a/002_source/cms/src/app/dashboard/connectives/create/page.tsx b/002_source/cms/src/app/dashboard/connectives/create/page.tsx new file mode 100644 index 0000000..fd45b0a --- /dev/null +++ b/002_source/cms/src/app/dashboard/connectives/create/page.tsx @@ -0,0 +1,48 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { LessonCategoryCreateForm } from '@/components/dashboard/lesson_category/lesson-category-create-form'; + +export default function Page(): React.JSX.Element { + const { t } = useTranslation(); + return ( + + + +
+ + + {t('title', { ns: 'lesson_category' })} + +
+
+ {t('create.title', { ns: 'lesson_category' })} +
+
+ +
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/connectives/edit/[cat_id]/_PROMPT.md b/002_source/cms/src/app/dashboard/connectives/edit/[cat_id]/_PROMPT.md new file mode 100644 index 0000000..abf4465 --- /dev/null +++ b/002_source/cms/src/app/dashboard/connectives/edit/[cat_id]/_PROMPT.md @@ -0,0 +1,11 @@ +# task + +## instruction + +with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/_helloworld/page.tsx` + +with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_types/edit/[typeId]/page.tsx` + +please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_categories/edit/page.tsx` + +please draft a tsx for showing error to user thanks, diff --git a/002_source/cms/src/app/dashboard/connectives/edit/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/connectives/edit/[cat_id]/page.tsx new file mode 100644 index 0000000..88737a6 --- /dev/null +++ b/002_source/cms/src/app/dashboard/connectives/edit/[cat_id]/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { LessonCategoryEditForm } from '@/components/dashboard/lesson_category/lesson-category-edit-form'; + +export default function Page(): React.JSX.Element { + const { t } = useTranslation(['lesson_category']); + + return ( + + + +
+ + + {t('edit.title', { ns: 'lesson_category' })} + +
+
+ {t('edit.title', { ns: 'lesson_category' })} +
+
+ +
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/connectives/lesson-categories-sample-data.tsx b/002_source/cms/src/app/dashboard/connectives/lesson-categories-sample-data.tsx new file mode 100644 index 0000000..6a815ad --- /dev/null +++ b/002_source/cms/src/app/dashboard/connectives/lesson-categories-sample-data.tsx @@ -0,0 +1,95 @@ +import { dayjs } from '@/lib/dayjs'; +import { LessonCategory } from '@/components/dashboard/lesson_category/type'; + +// import type { LessonCategory } from '@/components/dashboard/lp_categories/type'; + +// import type { LessonCategory } from '@/components/dashboard/lesson_category/lesson-categories-table'; +// import type { LessonCategory } from '@/components/dashboard/lesson_category/interfaces'; + +export const lessonCategoriesSampleData = [ + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, +] satisfies LessonCategory[]; diff --git a/002_source/cms/src/app/dashboard/connectives/list/page.tsx b/002_source/cms/src/app/dashboard/connectives/list/page.tsx new file mode 100644 index 0000000..375b163 --- /dev/null +++ b/002_source/cms/src/app/dashboard/connectives/list/page.tsx @@ -0,0 +1,302 @@ +'use client'; + +// RULES: +// contains list page for lesson_categories (LessonCategories) +// contain definition to collection only +// +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_CATEGORIES } from '@/constants'; +import { LoadingButton } from '@mui/lab'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Divider from '@mui/material/Divider'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import type { ListResult, RecordModel } from 'pocketbase'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import isDevelopment from '@/lib/check-is-development'; +import { logger } from '@/lib/default-logger'; +import { pb } from '@/lib/pb'; +import { toast } from '@/components/core/toaster'; +import ErrorDisplay from '@/components/dashboard/error'; +import { defaultLessonCategory } from '@/components/dashboard/lesson_category/_constants'; +import { LessonCategoriesFilters } from '@/components/dashboard/lesson_category/lesson-categories-filters'; +import type { Filters } from '@/components/dashboard/lesson_category/lesson-categories-filters'; +import { LessonCategoriesPagination } from '@/components/dashboard/lesson_category/lesson-categories-pagination'; +import { LessonCategoriesSelectionProvider } from '@/components/dashboard/lesson_category/lesson-categories-selection-context'; +import { LessonCategoriesTable } from '@/components/dashboard/lesson_category/lesson-categories-table'; +import type { LessonCategory } from '@/components/dashboard/lesson_category/type'; +// import type { LessonCategory } from '@/components/dashboard/lp_categories/type'; +import FormLoading from '@/components/loading'; + +// import { lessonCategoriesSampleData } from './lesson-categories-sample-data'; + +export default function Page({ searchParams }: PageProps): React.JSX.Element { + const { t } = useTranslation(['lesson_category']); + const { email, phone, sortDir, status, name, visible, type } = searchParams; + const router = useRouter(); + const [lessonCategoriesData, setLessonCategoriesData] = React.useState([]); + // + + const [isLoadingAddPage, setIsLoadingAddPage] = React.useState(false); + const [showLoading, setShowLoading] = React.useState(true); + const [showError, setShowError] = React.useState({ show: false, detail: '' }); + // + const [rowsPerPage, setRowsPerPage] = React.useState(5); + // + const [f, setF] = React.useState([]); + const [currentPage, setCurrentPage] = React.useState(1); + // + const [recordCount, setRecordCount] = React.useState(0); + const [listOption, setListOption] = React.useState({}); + const [listSort, setListSort] = React.useState({}); + + // + const sortedLessonCategories = applySort(lessonCategoriesData, sortDir); + const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status }); + + // + const reloadRows = async (): Promise => { + try { + const models: ListResult = await pb + .collection(COL_LESSON_CATEGORIES) + .getList(currentPage + 1, rowsPerPage, listOption); + const { items, totalItems } = models; + const tempLessonTypes: LessonCategory[] = items.map((lt) => { + return { ...defaultLessonCategory, ...lt }; + }); + + setLessonCategoriesData(tempLessonTypes); + setRecordCount(totalItems); + setF(tempLessonTypes); + // console.log({ currentPage, f }); + } catch (error) { + // + logger.error(error); + setShowError({ + // + show: true, + detail: JSON.stringify(error, null, 2), + }); + } finally { + setShowLoading(false); + } + + // pb.collection(COL_LESSON_CATEGORIES) + // .getList(currentPage, rowsPerPage, listOption) + // .then((lessonCategories: ListResult) => { + // // console.log(lessonTypes); + // const { items, page, perPage, totalItems, totalPages } = lessonCategories; + // const tempLessonCategories: LessonCategory[] = items.map((item) => { + // return { ...defaultLessonCategory, ...item }; + // }); + + // setLessonCategoriesData(tempLessonCategories); + // setRecordCount(totalItems); + // setF(tempLessonCategories); + // // console.log({ currentPage, f }); + // }) + // .catch((error) => { + // logger.error(error); + // setShowError({ + // // + // show: true, + // detail: JSON.stringify(error, null, 2), + // }); + // }) + // .finally(() => { + // setShowLoading(false); + // }); + }; + + const [lastListOption, setLastListOption] = React.useState({}); + const isFirstRun = React.useRef(false); + React.useEffect(() => { + if (!isFirstRun.current) { + isFirstRun.current = true; + } else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { + // reset page number as tab changes + setLastListOption(listOption); + setCurrentPage(0); + void reloadRows(); + } else { + void reloadRows(); + } + }, [currentPage, rowsPerPage, listOption]); + + React.useEffect(() => { + let tempFilter = [], + tempSortDir = ''; + + if (visible) { + tempFilter.push(`visible = "${visible}"`); + } + + if (sortDir) { + tempSortDir = `-created`; + } + + if (name) { + tempFilter.push(`name ~ "%${name}%"`); + } + + if (type) { + tempFilter.push(`type ~ "%${type}%"`); + } + + let preFinalListOption = {}; + if (tempFilter.length > 0) { + preFinalListOption = { filter: tempFilter.join(' && ') }; + } + if (tempSortDir.length > 0) { + preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; + } + setListOption(preFinalListOption); + // setListOption({ + // filter: tempFilter.join(' && '), + // sort: tempSortDir, + // // + // }); + }, [visible, sortDir, name, type]); + + // return <>helloworld; + + if (showLoading) return ; + + if (showError.show) + return ( + + ); + + return ( + + + + + {t('list.title')} + + + { + setIsLoadingAddPage(true); + router.push(paths.dashboard.lesson_categories.create); + }} + startIcon={} + variant="contained" + > + {/* add new lesson type */} + {t('list.add')} + + + + + + + + + + + + + + + + +
{JSON.stringify(f, null, 2)}
+
+
+ ); +} + +// Sorting and filtering has to be done on the server. + +function applySort(row: LessonCategory[], sortDir: 'asc' | 'desc' | undefined): LessonCategory[] { + return row.sort((a, b) => { + if (sortDir === 'asc') { + return a.createdAt.getTime() - b.createdAt.getTime(); + } + + return b.createdAt.getTime() - a.createdAt.getTime(); + }); +} + +function applyFilters(row: LessonCategory[], { email, phone, status, name, visible }: Filters): LessonCategory[] { + return row.filter((item) => { + if (email) { + if (!item.email?.toLowerCase().includes(email.toLowerCase())) { + return false; + } + } + + if (phone) { + if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { + return false; + } + } + + if (status) { + if (item.status !== status) { + return false; + } + } + + if (name) { + if (!item.name?.toLowerCase().includes(name.toLowerCase())) { + return false; + } + } + + if (visible) { + if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) { + return false; + } + } + + return true; + }); +} + +interface PageProps { + searchParams: { + email?: string; + phone?: string; + sortDir?: 'asc' | 'desc'; + status?: string; + name?: string; + visible?: string; + type?: string; + }; +} diff --git a/002_source/cms/src/app/dashboard/connectives/view/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/connectives/view/[cat_id]/page.tsx new file mode 100644 index 0000000..cc14c52 --- /dev/null +++ b/002_source/cms/src/app/dashboard/connectives/view/[cat_id]/page.tsx @@ -0,0 +1,383 @@ +'use client'; + +import * as React from 'react'; +// import type { Metadata } from 'next'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +import { COL_LESSON_CATEGORIES } from '@/constants'; +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 IconButton from '@mui/material/IconButton'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard'; +import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning'; +import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; +import { RecordModel } from 'pocketbase'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { logger } from '@/lib/default-logger'; +import { pb } from '@/lib/pb'; +import { PropertyItem } from '@/components/core/property-item'; +import { PropertyList } from '@/components/core/property-list'; +import { toast } from '@/components/core/toaster'; +import ErrorDisplay from '@/components/dashboard/error'; +import { defaultLessonCategory } from '@/components/dashboard/lesson_category/_constants.ts'; +// import { defaultLessonCategory } from '@/components/dashboard/lesson_category/defaultLessonCategory'; +import { Notifications } from '@/components/dashboard/lesson_category/notifications'; +import { Payments } from '@/components/dashboard/lesson_category/payments'; +import type { Address } from '@/components/dashboard/lesson_category/shipping-address'; +import { ShippingAddress } from '@/components/dashboard/lesson_category/shipping-address'; +import { LessonCategory } from '@/components/dashboard/lesson_category/type'; +// import type { LessonCategory } from '@/components/dashboard/lp_categories/type'; +import FormLoading from '@/components/loading'; + +// export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; + +export default function Page(): React.JSX.Element { + const { t } = useTranslation(); + const router = useRouter(); + // + const { cat_id: catId } = useParams<{ cat_id: string }>(); + // + const [showLoading, setShowLoading] = React.useState(true); + const [showError, setShowError] = React.useState(false); + // + const [showLessonCategory, setShowLessonCategory] = React.useState(defaultLessonCategory); + + function handleEditClick() { + router.push(paths.dashboard.lesson_categories.edit(showLessonCategory.id)); + } + + React.useEffect(() => { + if (catId) { + pb.collection(COL_LESSON_CATEGORIES) + .getOne(catId) + .then((model: RecordModel) => { + setShowLessonCategory({ ...defaultLessonCategory, ...model }); + }) + .catch((err) => { + logger.error(err); + toast(t('dashboard.lessonTypes.list.error')); + + setShowError(true); + }) + .finally(() => { + setShowLoading(false); + }); + } + }, [catId]); + + if (showLoading) return ; + + if (showError) + return ( + + ); + + return ( + + + +
+ + + {t('dashboard.lessonCategorys.list.title')} + +
+ + + + empty + +
+ + {showLessonCategory.name} + } + label={showLessonCategory.visible} + size="small" + variant="outlined" + /> + + + {showLessonCategory.id} + +
+
+
+ +
+
+
+ + + + + { + handleEditClick(); + }} + > + + + } + avatar={ + + + + } + title={t('basic-details', { ns: 'lesson_category' })} + /> + } + orientation="vertical" + sx={{ '--PropertyItem-padding': '12px 24px' }} + > + {( + [ + { key: 'Customer ID', value: }, + { key: 'Name', value: showLessonCategory.name }, + { key: 'Pos', value: showLessonCategory.pos }, + { + key: 'Visible', + value: ( + + ), + }, + { + key: 'Quota', + value: ( + + + + 50% + + + ), + }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + + + + + } + title={t('security', { ns: 'lesson_category' })} + /> + + +
+ +
+ + A deleted lesson category cannot be restored. All data will be permanently removed. + +
+
+
+
+
+ + + + + }> + Edit + + } + avatar={ + + + + } + title={t('billing-details', { ns: 'lesson_category' })} + /> + + + } sx={{ '--PropertyItem-padding': '16px' }}> + {( + [ + { key: 'Credit card', value: '**** 4142' }, + { key: 'Country', value: 'United States' }, + { key: 'State', value: 'Michigan' }, + { key: 'City', value: 'Southfield' }, + { key: 'Address', value: '1721 Bartlett Avenue, 48034' }, + { key: 'Tax ID', value: 'EU87956621' }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + + + + }> + Add + + } + avatar={ + + + + } + title={t('shipping-addresses', { ns: 'lesson_category' })} + /> + + + {( + [ + { + id: 'ADR-001', + country: 'United States', + state: 'Michigan', + city: 'Lansing', + zipCode: '48933', + street: '480 Haven Lane', + primary: true, + }, + { + id: 'ADR-002', + country: 'United States', + state: 'Missouri', + city: 'Springfield', + zipCode: '65804', + street: '4807 Lighthouse Drive', + }, + ] satisfies Address[] + ).map((address) => ( + + + + ))} + + + + + + +
+
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/vocabularies/create/page.tsx b/002_source/cms/src/app/dashboard/vocabularies/create/page.tsx new file mode 100644 index 0000000..fd45b0a --- /dev/null +++ b/002_source/cms/src/app/dashboard/vocabularies/create/page.tsx @@ -0,0 +1,48 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { LessonCategoryCreateForm } from '@/components/dashboard/lesson_category/lesson-category-create-form'; + +export default function Page(): React.JSX.Element { + const { t } = useTranslation(); + return ( + + + +
+ + + {t('title', { ns: 'lesson_category' })} + +
+
+ {t('create.title', { ns: 'lesson_category' })} +
+
+ +
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/vocabularies/edit/[cat_id]/_PROMPT.md b/002_source/cms/src/app/dashboard/vocabularies/edit/[cat_id]/_PROMPT.md new file mode 100644 index 0000000..abf4465 --- /dev/null +++ b/002_source/cms/src/app/dashboard/vocabularies/edit/[cat_id]/_PROMPT.md @@ -0,0 +1,11 @@ +# task + +## instruction + +with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/_helloworld/page.tsx` + +with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_types/edit/[typeId]/page.tsx` + +please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_categories/edit/page.tsx` + +please draft a tsx for showing error to user thanks, diff --git a/002_source/cms/src/app/dashboard/vocabularies/edit/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/vocabularies/edit/[cat_id]/page.tsx new file mode 100644 index 0000000..88737a6 --- /dev/null +++ b/002_source/cms/src/app/dashboard/vocabularies/edit/[cat_id]/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { LessonCategoryEditForm } from '@/components/dashboard/lesson_category/lesson-category-edit-form'; + +export default function Page(): React.JSX.Element { + const { t } = useTranslation(['lesson_category']); + + return ( + + + +
+ + + {t('edit.title', { ns: 'lesson_category' })} + +
+
+ {t('edit.title', { ns: 'lesson_category' })} +
+
+ +
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/vocabularies/lesson-categories-sample-data.tsx b/002_source/cms/src/app/dashboard/vocabularies/lesson-categories-sample-data.tsx new file mode 100644 index 0000000..6a815ad --- /dev/null +++ b/002_source/cms/src/app/dashboard/vocabularies/lesson-categories-sample-data.tsx @@ -0,0 +1,95 @@ +import { dayjs } from '@/lib/dayjs'; +import { LessonCategory } from '@/components/dashboard/lesson_category/type'; + +// import type { LessonCategory } from '@/components/dashboard/lp_categories/type'; + +// import type { LessonCategory } from '@/components/dashboard/lesson_category/lesson-categories-table'; +// import type { LessonCategory } from '@/components/dashboard/lesson_category/interfaces'; + +export const lessonCategoriesSampleData = [ + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + collectionId: '0000000001', + cat_name: '', + pos: 99, + visible: 'visible', + lesson_id: 'lid_00001', + description: '', + remarks: '', + }, +] satisfies LessonCategory[]; diff --git a/002_source/cms/src/app/dashboard/vocabularies/list/page.tsx b/002_source/cms/src/app/dashboard/vocabularies/list/page.tsx new file mode 100644 index 0000000..375b163 --- /dev/null +++ b/002_source/cms/src/app/dashboard/vocabularies/list/page.tsx @@ -0,0 +1,302 @@ +'use client'; + +// RULES: +// contains list page for lesson_categories (LessonCategories) +// contain definition to collection only +// +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_CATEGORIES } from '@/constants'; +import { LoadingButton } from '@mui/lab'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Divider from '@mui/material/Divider'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import type { ListResult, RecordModel } from 'pocketbase'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import isDevelopment from '@/lib/check-is-development'; +import { logger } from '@/lib/default-logger'; +import { pb } from '@/lib/pb'; +import { toast } from '@/components/core/toaster'; +import ErrorDisplay from '@/components/dashboard/error'; +import { defaultLessonCategory } from '@/components/dashboard/lesson_category/_constants'; +import { LessonCategoriesFilters } from '@/components/dashboard/lesson_category/lesson-categories-filters'; +import type { Filters } from '@/components/dashboard/lesson_category/lesson-categories-filters'; +import { LessonCategoriesPagination } from '@/components/dashboard/lesson_category/lesson-categories-pagination'; +import { LessonCategoriesSelectionProvider } from '@/components/dashboard/lesson_category/lesson-categories-selection-context'; +import { LessonCategoriesTable } from '@/components/dashboard/lesson_category/lesson-categories-table'; +import type { LessonCategory } from '@/components/dashboard/lesson_category/type'; +// import type { LessonCategory } from '@/components/dashboard/lp_categories/type'; +import FormLoading from '@/components/loading'; + +// import { lessonCategoriesSampleData } from './lesson-categories-sample-data'; + +export default function Page({ searchParams }: PageProps): React.JSX.Element { + const { t } = useTranslation(['lesson_category']); + const { email, phone, sortDir, status, name, visible, type } = searchParams; + const router = useRouter(); + const [lessonCategoriesData, setLessonCategoriesData] = React.useState([]); + // + + const [isLoadingAddPage, setIsLoadingAddPage] = React.useState(false); + const [showLoading, setShowLoading] = React.useState(true); + const [showError, setShowError] = React.useState({ show: false, detail: '' }); + // + const [rowsPerPage, setRowsPerPage] = React.useState(5); + // + const [f, setF] = React.useState([]); + const [currentPage, setCurrentPage] = React.useState(1); + // + const [recordCount, setRecordCount] = React.useState(0); + const [listOption, setListOption] = React.useState({}); + const [listSort, setListSort] = React.useState({}); + + // + const sortedLessonCategories = applySort(lessonCategoriesData, sortDir); + const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status }); + + // + const reloadRows = async (): Promise => { + try { + const models: ListResult = await pb + .collection(COL_LESSON_CATEGORIES) + .getList(currentPage + 1, rowsPerPage, listOption); + const { items, totalItems } = models; + const tempLessonTypes: LessonCategory[] = items.map((lt) => { + return { ...defaultLessonCategory, ...lt }; + }); + + setLessonCategoriesData(tempLessonTypes); + setRecordCount(totalItems); + setF(tempLessonTypes); + // console.log({ currentPage, f }); + } catch (error) { + // + logger.error(error); + setShowError({ + // + show: true, + detail: JSON.stringify(error, null, 2), + }); + } finally { + setShowLoading(false); + } + + // pb.collection(COL_LESSON_CATEGORIES) + // .getList(currentPage, rowsPerPage, listOption) + // .then((lessonCategories: ListResult) => { + // // console.log(lessonTypes); + // const { items, page, perPage, totalItems, totalPages } = lessonCategories; + // const tempLessonCategories: LessonCategory[] = items.map((item) => { + // return { ...defaultLessonCategory, ...item }; + // }); + + // setLessonCategoriesData(tempLessonCategories); + // setRecordCount(totalItems); + // setF(tempLessonCategories); + // // console.log({ currentPage, f }); + // }) + // .catch((error) => { + // logger.error(error); + // setShowError({ + // // + // show: true, + // detail: JSON.stringify(error, null, 2), + // }); + // }) + // .finally(() => { + // setShowLoading(false); + // }); + }; + + const [lastListOption, setLastListOption] = React.useState({}); + const isFirstRun = React.useRef(false); + React.useEffect(() => { + if (!isFirstRun.current) { + isFirstRun.current = true; + } else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) { + // reset page number as tab changes + setLastListOption(listOption); + setCurrentPage(0); + void reloadRows(); + } else { + void reloadRows(); + } + }, [currentPage, rowsPerPage, listOption]); + + React.useEffect(() => { + let tempFilter = [], + tempSortDir = ''; + + if (visible) { + tempFilter.push(`visible = "${visible}"`); + } + + if (sortDir) { + tempSortDir = `-created`; + } + + if (name) { + tempFilter.push(`name ~ "%${name}%"`); + } + + if (type) { + tempFilter.push(`type ~ "%${type}%"`); + } + + let preFinalListOption = {}; + if (tempFilter.length > 0) { + preFinalListOption = { filter: tempFilter.join(' && ') }; + } + if (tempSortDir.length > 0) { + preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; + } + setListOption(preFinalListOption); + // setListOption({ + // filter: tempFilter.join(' && '), + // sort: tempSortDir, + // // + // }); + }, [visible, sortDir, name, type]); + + // return <>helloworld; + + if (showLoading) return ; + + if (showError.show) + return ( + + ); + + return ( + + + + + {t('list.title')} + + + { + setIsLoadingAddPage(true); + router.push(paths.dashboard.lesson_categories.create); + }} + startIcon={} + variant="contained" + > + {/* add new lesson type */} + {t('list.add')} + + + + + + + + + + + + + + + + +
{JSON.stringify(f, null, 2)}
+
+
+ ); +} + +// Sorting and filtering has to be done on the server. + +function applySort(row: LessonCategory[], sortDir: 'asc' | 'desc' | undefined): LessonCategory[] { + return row.sort((a, b) => { + if (sortDir === 'asc') { + return a.createdAt.getTime() - b.createdAt.getTime(); + } + + return b.createdAt.getTime() - a.createdAt.getTime(); + }); +} + +function applyFilters(row: LessonCategory[], { email, phone, status, name, visible }: Filters): LessonCategory[] { + return row.filter((item) => { + if (email) { + if (!item.email?.toLowerCase().includes(email.toLowerCase())) { + return false; + } + } + + if (phone) { + if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { + return false; + } + } + + if (status) { + if (item.status !== status) { + return false; + } + } + + if (name) { + if (!item.name?.toLowerCase().includes(name.toLowerCase())) { + return false; + } + } + + if (visible) { + if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) { + return false; + } + } + + return true; + }); +} + +interface PageProps { + searchParams: { + email?: string; + phone?: string; + sortDir?: 'asc' | 'desc'; + status?: string; + name?: string; + visible?: string; + type?: string; + }; +} diff --git a/002_source/cms/src/app/dashboard/vocabularies/view/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/vocabularies/view/[cat_id]/page.tsx new file mode 100644 index 0000000..cc14c52 --- /dev/null +++ b/002_source/cms/src/app/dashboard/vocabularies/view/[cat_id]/page.tsx @@ -0,0 +1,383 @@ +'use client'; + +import * as React from 'react'; +// import type { Metadata } from 'next'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +import { COL_LESSON_CATEGORIES } from '@/constants'; +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 IconButton from '@mui/material/IconButton'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard'; +import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning'; +import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; +import { RecordModel } from 'pocketbase'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { logger } from '@/lib/default-logger'; +import { pb } from '@/lib/pb'; +import { PropertyItem } from '@/components/core/property-item'; +import { PropertyList } from '@/components/core/property-list'; +import { toast } from '@/components/core/toaster'; +import ErrorDisplay from '@/components/dashboard/error'; +import { defaultLessonCategory } from '@/components/dashboard/lesson_category/_constants.ts'; +// import { defaultLessonCategory } from '@/components/dashboard/lesson_category/defaultLessonCategory'; +import { Notifications } from '@/components/dashboard/lesson_category/notifications'; +import { Payments } from '@/components/dashboard/lesson_category/payments'; +import type { Address } from '@/components/dashboard/lesson_category/shipping-address'; +import { ShippingAddress } from '@/components/dashboard/lesson_category/shipping-address'; +import { LessonCategory } from '@/components/dashboard/lesson_category/type'; +// import type { LessonCategory } from '@/components/dashboard/lp_categories/type'; +import FormLoading from '@/components/loading'; + +// export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; + +export default function Page(): React.JSX.Element { + const { t } = useTranslation(); + const router = useRouter(); + // + const { cat_id: catId } = useParams<{ cat_id: string }>(); + // + const [showLoading, setShowLoading] = React.useState(true); + const [showError, setShowError] = React.useState(false); + // + const [showLessonCategory, setShowLessonCategory] = React.useState(defaultLessonCategory); + + function handleEditClick() { + router.push(paths.dashboard.lesson_categories.edit(showLessonCategory.id)); + } + + React.useEffect(() => { + if (catId) { + pb.collection(COL_LESSON_CATEGORIES) + .getOne(catId) + .then((model: RecordModel) => { + setShowLessonCategory({ ...defaultLessonCategory, ...model }); + }) + .catch((err) => { + logger.error(err); + toast(t('dashboard.lessonTypes.list.error')); + + setShowError(true); + }) + .finally(() => { + setShowLoading(false); + }); + } + }, [catId]); + + if (showLoading) return ; + + if (showError) + return ( + + ); + + return ( + + + +
+ + + {t('dashboard.lessonCategorys.list.title')} + +
+ + + + empty + +
+ + {showLessonCategory.name} + } + label={showLessonCategory.visible} + size="small" + variant="outlined" + /> + + + {showLessonCategory.id} + +
+
+
+ +
+
+
+ + + + + { + handleEditClick(); + }} + > + + + } + avatar={ + + + + } + title={t('basic-details', { ns: 'lesson_category' })} + /> + } + orientation="vertical" + sx={{ '--PropertyItem-padding': '12px 24px' }} + > + {( + [ + { key: 'Customer ID', value: }, + { key: 'Name', value: showLessonCategory.name }, + { key: 'Pos', value: showLessonCategory.pos }, + { + key: 'Visible', + value: ( + + ), + }, + { + key: 'Quota', + value: ( + + + + 50% + + + ), + }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + + + + + } + title={t('security', { ns: 'lesson_category' })} + /> + + +
+ +
+ + A deleted lesson category cannot be restored. All data will be permanently removed. + +
+
+
+
+
+ + + + + }> + Edit + + } + avatar={ + + + + } + title={t('billing-details', { ns: 'lesson_category' })} + /> + + + } sx={{ '--PropertyItem-padding': '16px' }}> + {( + [ + { key: 'Credit card', value: '**** 4142' }, + { key: 'Country', value: 'United States' }, + { key: 'State', value: 'Michigan' }, + { key: 'City', value: 'Southfield' }, + { key: 'Address', value: '1721 Bartlett Avenue, 48034' }, + { key: 'Tax ID', value: 'EU87956621' }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + + + + }> + Add + + } + avatar={ + + + + } + title={t('shipping-addresses', { ns: 'lesson_category' })} + /> + + + {( + [ + { + id: 'ADR-001', + country: 'United States', + state: 'Michigan', + city: 'Lansing', + zipCode: '48933', + street: '480 Haven Lane', + primary: true, + }, + { + id: 'ADR-002', + country: 'United States', + state: 'Missouri', + city: 'Springfield', + zipCode: '65804', + street: '4807 Lighthouse Drive', + }, + ] satisfies Address[] + ).map((address) => ( + + + + ))} + + + + + + +
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/connective/_constants.ts b/002_source/cms/src/components/dashboard/connective/_constants.ts new file mode 100644 index 0000000..1458199 --- /dev/null +++ b/002_source/cms/src/components/dashboard/connective/_constants.ts @@ -0,0 +1,44 @@ +import { dayjs } from '@/lib/dayjs'; + +import { CreateForm, LessonCategory } from './type'; + +// import type { CreateForm, LessonCategory } from '../lp_categories/type'; + +export const defaultLessonCategory: LessonCategory = { + 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', + // + collectionId: '0000000000', + createdAt: dayjs('2099-01-01').toDate(), + // + name: '', + avatar: '', + email: '', + phone: '', + quota: 0, + status: 'NA', +}; + +export const LessonCategoryCreateFormDefault: CreateForm = { + name: '', + type: '', + pos: 1, + visible: 'visible', + description: '', + isActive: true, + order: 1, + imageUrl: '', +}; + +export const emptyLessonCategory: LessonCategory = { + ...defaultLessonCategory, + isEmpty: true, +}; diff --git a/002_source/cms/src/components/dashboard/connective/confirm-delete-modal.tsx b/002_source/cms/src/components/dashboard/connective/confirm-delete-modal.tsx new file mode 100644 index 0000000..7f0ab5f --- /dev/null +++ b/002_source/cms/src/components/dashboard/connective/confirm-delete-modal.tsx @@ -0,0 +1,123 @@ +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_TYPES } from '@/constants'; +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'; + +const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL); + +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 performDelete(id: string): Promise { + return pb + .collection(COL_LESSON_TYPES) + .delete(id) + .then(() => { + toast(t('dashboard.lessonTypes.delete.success')); + reloadRows(); + }) + .catch((err) => { + logger.error(err); + toast(t('dashboard.lessonTypes.delete.error')); + }) + .finally(() => {}); + } + + function handleUserConfirmDelete(): void { + if (idToDelete) { + setIsDeleteing(true); + performDelete(idToDelete) + .then(() => { + handleClose(); + setIsDeleteing(false); + }) + .catch((err) => { + // console.error(err) + logger.error(err); + toast(t('dashboard.lessonTypes.delete.error')); + }); + } + } + + 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/connective/helloworld.tsx b/002_source/cms/src/components/dashboard/connective/helloworld.tsx new file mode 100644 index 0000000..3989cb1 --- /dev/null +++ b/002_source/cms/src/components/dashboard/connective/helloworld.tsx @@ -0,0 +1,3 @@ +const helloworld = 'helloworld'; + +export { helloworld }; diff --git a/002_source/cms/src/components/dashboard/connective/interfaces.1ts b/002_source/cms/src/components/dashboard/connective/interfaces.1ts new file mode 100644 index 0000000..5973b06 --- /dev/null +++ b/002_source/cms/src/components/dashboard/connective/interfaces.1ts @@ -0,0 +1,64 @@ +import { dayjs } from '@/lib/dayjs'; + +export interface LessonCategory { + isEmpty?: boolean; + // + id: string; + cat_name: string; + cat_image_url?: string; + cat_image?: string; + pos: number; + visible: string; + lesson_id: string; + description: string; + remarks: string; + // + name: string; + avatar: string; + email: string; + phone: string; + quota: number; + status: 'pending' | 'active' | 'blocked' | 'NA'; + createdAt: Date; +} + +export const defaultLessonCategory: LessonCategory = { + 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', + // + createdAt: dayjs('2099-01-01').toDate(), + // + name: '', + avatar: '', + email: '', + phone: '', + quota: 0, + status: 'NA', +}; + +export const emptyLessonCategory: LessonCategory = { + ...defaultLessonCategory, + isEmpty: true, +}; + +export interface CreateForm { + name: string; + type: string; + pos: number; + visible: string; +} + +export const LessonCategoryCreateFormDefault: CreateForm = { + name: '', + type: '', + pos: 1, + visible: 'visible', +}; diff --git a/002_source/cms/src/components/dashboard/connective/lesson-categories-filters.tsx b/002_source/cms/src/components/dashboard/connective/lesson-categories-filters.tsx new file mode 100644 index 0000000..ec387a5 --- /dev/null +++ b/002_source/cms/src/components/dashboard/connective/lesson-categories-filters.tsx @@ -0,0 +1,401 @@ +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_CATEGORIES } from '@/constants'; +import GetAllCount from '@/db/LessonCategories/GetAllCount'; +import GetHiddenCount from '@/db/LessonCategories/GetHiddenCount'; +import GetVisibleCount from '@/db/LessonCategories/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 { pb } from '@/lib/pb'; +import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; +import { Option } from '@/components/core/option'; + +// import { LessonCategory } from '../lp_categories/type'; +import { useLessonCategoriesSelection } from './lesson-categories-selection-context'; +import { LessonCategory } from './type'; + +export interface Filters { + email?: string; + phone?: string; + status?: string; + name?: string; + visible?: string; + type?: string; +} + +export type SortDir = 'asc' | 'desc'; + +export interface LessonCategoriesFiltersProps { + filters?: Filters; + sortDir?: SortDir; + fullData: LessonCategory[]; +} + +export function LessonCategoriesFilters({ + filters = {}, + sortDir = 'desc', + fullData, +}: LessonCategoriesFiltersProps): 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 = useLessonCategoriesSelection(); + + function getVisible(): number { + return fullData.reduce((count, item: LessonCategory) => { + return item.visible === 'visible' ? count + 1 : count; + }, 0); + } + + function getHidden(): number { + return fullData.reduce((count, item: LessonCategory) => { + 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); + } + + router.push(`${paths.dashboard.lesson_categories.list}?${searchParams.toString()}`); + }, + [router] + ); + + const handleClearFilters = React.useCallback(() => { + updateSearchParams({}, sortDir); + }, [updateSearchParams, sortDir]); + + const handleStatusChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, status: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const 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(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function PhoneFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} diff --git a/002_source/cms/src/components/dashboard/connective/lesson-categories-pagination.tsx b/002_source/cms/src/components/dashboard/connective/lesson-categories-pagination.tsx new file mode 100644 index 0000000..7a344d5 --- /dev/null +++ b/002_source/cms/src/components/dashboard/connective/lesson-categories-pagination.tsx @@ -0,0 +1,52 @@ +'use client'; + +// lesson-categories-pagination.tsx +// RULES: +// T.B.A. +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 LessonCategoriesPagination({ + 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/connective/lesson-categories-selection-context.tsx b/002_source/cms/src/components/dashboard/connective/lesson-categories-selection-context.tsx new file mode 100644 index 0000000..693e000 --- /dev/null +++ b/002_source/cms/src/components/dashboard/connective/lesson-categories-selection-context.tsx @@ -0,0 +1,53 @@ +'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 { LessonCategory } from './type'; + +// import type { LessonCategory } from '../lp_categories/type'; + +// import type { LessonCategory } from './lesson-categories-table'; +// import type { LessonCategory } from '@/components/dashboard/lesson_category/interfaces'; + +function noop(): void { + return undefined; +} + +export interface LessonCategoriesSelectionContextValue extends Selection {} + +export const LessonCategoriesSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface LessonCategoriesSelectionProviderProps { + children: React.ReactNode; + lessonCategories: LessonCategory[]; +} + +export function LessonCategoriesSelectionProvider({ + children, + lessonCategories = [], +}: LessonCategoriesSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]); + const selection = useSelection(customerIds); + + return ( + + {children} + + ); +} + +export function useLessonCategoriesSelection(): LessonCategoriesSelectionContextValue { + return React.useContext(LessonCategoriesSelectionContext); +} diff --git a/002_source/cms/src/components/dashboard/connective/lesson-categories-table.tsx b/002_source/cms/src/components/dashboard/connective/lesson-categories-table.tsx new file mode 100644 index 0000000..f90667a --- /dev/null +++ b/002_source/cms/src/components/dashboard/connective/lesson-categories-table.tsx @@ -0,0 +1,244 @@ +'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 Chip from '@mui/material/Chip'; +import IconButton from '@mui/material/IconButton'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; +import { 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 { useLessonCategoriesSelection } from './lesson-categories-selection-context'; +import type { LessonCategory } 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: LessonCategory[]; + reloadRows: () => void; +} + +export function LessonCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element { + const { t } = useTranslation(['lesson_category']); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLessonCategoriesSelection(); + + 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 ? ( + + + {/* TODO: use hyphen here */} + {t('no-lesson-categories-found')} + + + ) : null} + + ); +} diff --git a/002_source/cms/src/components/dashboard/connective/lesson-category-create-form.tsx b/002_source/cms/src/components/dashboard/connective/lesson-category-create-form.tsx new file mode 100644 index 0000000..7744b52 --- /dev/null +++ b/002_source/cms/src/components/dashboard/connective/lesson-category-create-form.tsx @@ -0,0 +1,275 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_CATEGORIES, NS_LESSON_CATEGORY } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +import { Avatar, Divider, MenuItem } 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 Checkbox from '@mui/material/Checkbox'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +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 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 { logger } from '@/lib/default-logger'; +import { fileToBase64 } from '@/lib/file-to-base64'; +import { Option } from '@/components/core/option'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; + +const schema = zod.object({ + avatar: zod.string().optional(), + name: zod.string().min(1, 'Name is required').max(255), + email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255), + phone: zod.string().min(1, 'Phone is required').max(15), + company: zod.string().max(255), + billingAddress: zod.object({ + country: zod.string().min(1, 'Country is required').max(255), + state: zod.string().min(1, 'State is required').max(255), + city: zod.string().min(1, 'City is required').max(255), + zipCode: zod.string().min(1, 'Zip code is required').max(255), + line1: zod.string().min(1, 'Street line 1 is required').max(255), + line2: zod.string().max(255).optional(), + }), + taxId: zod.string().max(255).optional(), + timezone: zod.string().min(1, 'Timezone is required').max(255), + language: zod.string().min(1, 'Language is required').max(255), + currency: zod.string().min(1, 'Currency is required').max(255), +}); + +type Values = zod.infer; + +const defaultValues = { + avatar: '', + name: '', + email: '', + phone: '', + company: '', + billingAddress: { country: '', state: '', city: '', zipCode: '', line1: '', line2: '' }, + taxId: '', + timezone: 'new_york', + language: 'en', + currency: 'USD', +} satisfies Values; + +export function LessonCategoryCreateForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['common', 'lesson_category']); + + const NS_DEFAULT = { ns: 'lesson_category' }; + + 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); + try { + // Make API request + toast.success('Customer updated'); + router.push(paths.dashboard.lesson_categories.details('1')); + } catch (err) { + logger.error(err); + toast.error('Something went wrong!'); + } + }, + [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.typeInformation', NS_DEFAULT)} + + + + + + + + + + {t('create.avatar', NS_DEFAULT)} + {t('create.avatarRequirements', NS_DEFAULT)} + + + + + + + ( + + {t('create.name', NS_DEFAULT)} + + {errors.name ? {errors.name.message} : null} + + )} + /> + + + ( + + Email address + + {errors.email ? {errors.email.message} : null} + + )} + /> + + + ( + + Phone number + + {errors.phone ? {errors.phone.message} : null} + + )} + /> + + + ( + + Company + + {errors.company ? {errors.company.message} : null} + + )} + /> + + + + + {t('create.detail-information', NS_DEFAULT)} + + + ( + + + {t('create.description', NS_DEFAULT)} + + + + + + )} + /> + + + ( + + + {t('create.remarks', NS_DEFAULT)} + + + + + + )} + /> + + + + + + + + + + {t('create.createButton', NS_DEFAULT)} + + + +
+ ); +} diff --git a/002_source/cms/src/components/dashboard/connective/lesson-category-edit-form.tsx b/002_source/cms/src/components/dashboard/connective/lesson-category-edit-form.tsx new file mode 100644 index 0000000..2ec64e2 --- /dev/null +++ b/002_source/cms/src/components/dashboard/connective/lesson-category-edit-form.tsx @@ -0,0 +1,353 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +import { COL_LESSON_CATEGORIES, NS_LESSON_CATEGORY } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +import { Avatar, Divider, MenuItem } 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 Checkbox from '@mui/material/Checkbox'; + +import FormControl from '@mui/material/FormControl'; +// import FormControlLabel from '@mui/material/FormControlLabel'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +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 type { RecordModel } from 'pocketbase'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { logger } from '@/lib/default-logger'; +import { fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { Option } from '@/components/core/option'; +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 { defaultLessonCategory } from './_constants'; +import type { EditFormProps, LessonCategory } from './type'; + +// TODO: review this +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + // + pos: zod.number().min(1, 'Phone is required').max(99), + visible: zod.string().max(255), + // + description: zod.string().optional(), + remarks: zod.string().optional(), +}); + +type Values = zod.infer; + +const defaultValues = { + cat_name: '', + // cat_image: undefined, + pos: 0, + visible: 'hidden', + // lesson_id: 'default-lesson-id', + description: 'default-description', + remarks: 'default-remarks', + // +} satisfies Values; + +export function LessonCategoryEditForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['common', 'lesson_category']); + + const NS_DEFAULT = { ns: 'lesson_category' }; + + 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(false); + + 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, + pos: values.pos, + visible: values.visible, + description: values.description, + remarks: values.remarks, + type: '', + }; + + pb.collection(COL_LESSON_CATEGORIES) + .update(catId, tempUpdate) + .then((res) => { + logger.debug(res); + toast.success(t('update.success', NS_DEFAULT)); + router.push(paths.dashboard.lesson_categories.list); + }) + .catch((err) => { + logger.error(err); + toast.error('Something went wrong!'); + }) + .finally(() => { + // + setIsUpdating(false); + }); + }, []); + + 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] + ); + + const [textDescription, setTextDescription] = React.useState('loading'); + const [textRemarks, setTextRemarks] = React.useState('loading'); + const handleLoad = React.useCallback( + (id: string) => { + setShowLoading(true); + pb.collection(COL_LESSON_CATEGORIES) + .getOne(id) + .then((model: RecordModel) => { + const temp: LessonCategory = { ...defaultLessonCategory, ...model }; + reset(temp); + setTextDescription(temp.description); + setTextRemarks(temp.remarks); + }) + .catch((err) => { + logger.error(err); + toast(t('list.error', NS_DEFAULT)); + }) + .finally(() => { + setShowLoading(false); + }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [catId] + ); + + React.useEffect(() => { + handleLoad(catId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [catId]); + + if (showLoading) return ; + if (showError) + return ( + + ); + + return ( +
+ + + } spacing={4}> + + {t('edit.basic-info', NS_DEFAULT)} + + + + + {/* + + // TODO: resume me + + + */} + + + {t('edit.avatar', NS_DEFAULT)} + {t('edit.avatarRequirements', NS_DEFAULT)} + + + + + + + ( + + {t('edit.name', NS_DEFAULT)} + + {errors.cat_name ? {errors.cat_name.message} : null} + + )} + /> + + + + ( + + {t('edit.position', NS_DEFAULT)} + { + field.onChange(parseInt(e.target.value)); + }} + type="number" + /> + + {errors.pos ? {errors.pos.message} : null} + + )} + /> + + + ( + + {t('edit.visible', NS_DEFAULT)} + + + {errors.visible ? {errors.visible.message} : null} + + )} + /> + + + + + + {t('create.detail-information', NS_DEFAULT)} + + + { + return ( + + + {t('create.description', NS_DEFAULT)} + + + { + field.onChange({ target: { value: editor.getHTML() } }); + }} + placeholder={t('edit.write-something', NS_DEFAULT)} + /> + + + ); + }} + /> + + + ( + + + {t('create.remarks', NS_DEFAULT)} + + + { + field.onChange({ target: { value: editor.getText() } }); + }} + hideToolbar + placeholder={t('edit.write-something', NS_DEFAULT)} + /> + + + )} + /> + + + + + + + + + + {t('edit.updateButton', NS_DEFAULT)} + + + +
+ ); +} diff --git a/002_source/cms/src/components/dashboard/connective/notifications.tsx b/002_source/cms/src/components/dashboard/connective/notifications.tsx new file mode 100644 index 0000000..a6c16bd --- /dev/null +++ b/002_source/cms/src/components/dashboard/connective/notifications.tsx @@ -0,0 +1,101 @@ +'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/connective/payments.tsx b/002_source/cms/src/components/dashboard/connective/payments.tsx new file mode 100644 index 0000000..0420d32 --- /dev/null +++ b/002_source/cms/src/components/dashboard/connective/payments.tsx @@ -0,0 +1,138 @@ +'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/connective/shipping-address.tsx b/002_source/cms/src/components/dashboard/connective/shipping-address.tsx new file mode 100644 index 0000000..8793e5c --- /dev/null +++ b/002_source/cms/src/components/dashboard/connective/shipping-address.tsx @@ -0,0 +1,46 @@ +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/connective/type.d.ts b/002_source/cms/src/components/dashboard/connective/type.d.ts new file mode 100644 index 0000000..4ea1ce5 --- /dev/null +++ b/002_source/cms/src/components/dashboard/connective/type.d.ts @@ -0,0 +1,47 @@ +export interface LessonCategory { + 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; + createdAt: Date; + // + name: string; + avatar: string; + email: string; + phone: string; + quota: number; + status: 'pending' | 'active' | 'blocked' | 'NA'; +} + +export interface CreateForm { + name: string; + type: string; + pos: number; + visible: string; + description: string; + isActive: boolean; + order: number; + imageUrl: string; +} + +export interface EditFormProps { + cat_name: string; + pos: number; + visible: string; + description?: string; + remarks?: string; + type: string; +} + +export interface Helloworld { + helloworld: string; +} diff --git a/002_source/cms/src/components/dashboard/layout/config.ts b/002_source/cms/src/components/dashboard/layout/config.ts index 573f268..735b955 100644 --- a/002_source/cms/src/components/dashboard/layout/config.ts +++ b/002_source/cms/src/components/dashboard/layout/config.ts @@ -43,12 +43,12 @@ export const layoutConfig = { }, { key: 'vocabulary', - title: 'dashboard.vocabulary.list', - href: paths.dashboard.vocabulary.list, + title: 'dashboard.vocabularies.list', + href: paths.dashboard.vocabularies.list, }, { - key: 'connectives', - title: 'dashboard.connective.list', + key: 'connective', + title: 'dashboard.connectives.list', href: paths.dashboard.connectives.list, }, ], diff --git a/002_source/cms/src/components/dashboard/vocabulary/_constants.ts b/002_source/cms/src/components/dashboard/vocabulary/_constants.ts new file mode 100644 index 0000000..1458199 --- /dev/null +++ b/002_source/cms/src/components/dashboard/vocabulary/_constants.ts @@ -0,0 +1,44 @@ +import { dayjs } from '@/lib/dayjs'; + +import { CreateForm, LessonCategory } from './type'; + +// import type { CreateForm, LessonCategory } from '../lp_categories/type'; + +export const defaultLessonCategory: LessonCategory = { + 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', + // + collectionId: '0000000000', + createdAt: dayjs('2099-01-01').toDate(), + // + name: '', + avatar: '', + email: '', + phone: '', + quota: 0, + status: 'NA', +}; + +export const LessonCategoryCreateFormDefault: CreateForm = { + name: '', + type: '', + pos: 1, + visible: 'visible', + description: '', + isActive: true, + order: 1, + imageUrl: '', +}; + +export const emptyLessonCategory: LessonCategory = { + ...defaultLessonCategory, + isEmpty: true, +}; diff --git a/002_source/cms/src/components/dashboard/vocabulary/confirm-delete-modal.tsx b/002_source/cms/src/components/dashboard/vocabulary/confirm-delete-modal.tsx new file mode 100644 index 0000000..7f0ab5f --- /dev/null +++ b/002_source/cms/src/components/dashboard/vocabulary/confirm-delete-modal.tsx @@ -0,0 +1,123 @@ +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_TYPES } from '@/constants'; +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'; + +const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL); + +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 performDelete(id: string): Promise { + return pb + .collection(COL_LESSON_TYPES) + .delete(id) + .then(() => { + toast(t('dashboard.lessonTypes.delete.success')); + reloadRows(); + }) + .catch((err) => { + logger.error(err); + toast(t('dashboard.lessonTypes.delete.error')); + }) + .finally(() => {}); + } + + function handleUserConfirmDelete(): void { + if (idToDelete) { + setIsDeleteing(true); + performDelete(idToDelete) + .then(() => { + handleClose(); + setIsDeleteing(false); + }) + .catch((err) => { + // console.error(err) + logger.error(err); + toast(t('dashboard.lessonTypes.delete.error')); + }); + } + } + + 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/vocabulary/helloworld.tsx b/002_source/cms/src/components/dashboard/vocabulary/helloworld.tsx new file mode 100644 index 0000000..3989cb1 --- /dev/null +++ b/002_source/cms/src/components/dashboard/vocabulary/helloworld.tsx @@ -0,0 +1,3 @@ +const helloworld = 'helloworld'; + +export { helloworld }; diff --git a/002_source/cms/src/components/dashboard/vocabulary/interfaces.1ts b/002_source/cms/src/components/dashboard/vocabulary/interfaces.1ts new file mode 100644 index 0000000..5973b06 --- /dev/null +++ b/002_source/cms/src/components/dashboard/vocabulary/interfaces.1ts @@ -0,0 +1,64 @@ +import { dayjs } from '@/lib/dayjs'; + +export interface LessonCategory { + isEmpty?: boolean; + // + id: string; + cat_name: string; + cat_image_url?: string; + cat_image?: string; + pos: number; + visible: string; + lesson_id: string; + description: string; + remarks: string; + // + name: string; + avatar: string; + email: string; + phone: string; + quota: number; + status: 'pending' | 'active' | 'blocked' | 'NA'; + createdAt: Date; +} + +export const defaultLessonCategory: LessonCategory = { + 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', + // + createdAt: dayjs('2099-01-01').toDate(), + // + name: '', + avatar: '', + email: '', + phone: '', + quota: 0, + status: 'NA', +}; + +export const emptyLessonCategory: LessonCategory = { + ...defaultLessonCategory, + isEmpty: true, +}; + +export interface CreateForm { + name: string; + type: string; + pos: number; + visible: string; +} + +export const LessonCategoryCreateFormDefault: CreateForm = { + name: '', + type: '', + pos: 1, + visible: 'visible', +}; diff --git a/002_source/cms/src/components/dashboard/vocabulary/lesson-categories-filters.tsx b/002_source/cms/src/components/dashboard/vocabulary/lesson-categories-filters.tsx new file mode 100644 index 0000000..ec387a5 --- /dev/null +++ b/002_source/cms/src/components/dashboard/vocabulary/lesson-categories-filters.tsx @@ -0,0 +1,401 @@ +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_CATEGORIES } from '@/constants'; +import GetAllCount from '@/db/LessonCategories/GetAllCount'; +import GetHiddenCount from '@/db/LessonCategories/GetHiddenCount'; +import GetVisibleCount from '@/db/LessonCategories/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 { pb } from '@/lib/pb'; +import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; +import { Option } from '@/components/core/option'; + +// import { LessonCategory } from '../lp_categories/type'; +import { useLessonCategoriesSelection } from './lesson-categories-selection-context'; +import { LessonCategory } from './type'; + +export interface Filters { + email?: string; + phone?: string; + status?: string; + name?: string; + visible?: string; + type?: string; +} + +export type SortDir = 'asc' | 'desc'; + +export interface LessonCategoriesFiltersProps { + filters?: Filters; + sortDir?: SortDir; + fullData: LessonCategory[]; +} + +export function LessonCategoriesFilters({ + filters = {}, + sortDir = 'desc', + fullData, +}: LessonCategoriesFiltersProps): 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 = useLessonCategoriesSelection(); + + function getVisible(): number { + return fullData.reduce((count, item: LessonCategory) => { + return item.visible === 'visible' ? count + 1 : count; + }, 0); + } + + function getHidden(): number { + return fullData.reduce((count, item: LessonCategory) => { + 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); + } + + router.push(`${paths.dashboard.lesson_categories.list}?${searchParams.toString()}`); + }, + [router] + ); + + const handleClearFilters = React.useCallback(() => { + updateSearchParams({}, sortDir); + }, [updateSearchParams, sortDir]); + + const handleStatusChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, status: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const 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(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function PhoneFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} diff --git a/002_source/cms/src/components/dashboard/vocabulary/lesson-categories-pagination.tsx b/002_source/cms/src/components/dashboard/vocabulary/lesson-categories-pagination.tsx new file mode 100644 index 0000000..7a344d5 --- /dev/null +++ b/002_source/cms/src/components/dashboard/vocabulary/lesson-categories-pagination.tsx @@ -0,0 +1,52 @@ +'use client'; + +// lesson-categories-pagination.tsx +// RULES: +// T.B.A. +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 LessonCategoriesPagination({ + 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/vocabulary/lesson-categories-selection-context.tsx b/002_source/cms/src/components/dashboard/vocabulary/lesson-categories-selection-context.tsx new file mode 100644 index 0000000..693e000 --- /dev/null +++ b/002_source/cms/src/components/dashboard/vocabulary/lesson-categories-selection-context.tsx @@ -0,0 +1,53 @@ +'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 { LessonCategory } from './type'; + +// import type { LessonCategory } from '../lp_categories/type'; + +// import type { LessonCategory } from './lesson-categories-table'; +// import type { LessonCategory } from '@/components/dashboard/lesson_category/interfaces'; + +function noop(): void { + return undefined; +} + +export interface LessonCategoriesSelectionContextValue extends Selection {} + +export const LessonCategoriesSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface LessonCategoriesSelectionProviderProps { + children: React.ReactNode; + lessonCategories: LessonCategory[]; +} + +export function LessonCategoriesSelectionProvider({ + children, + lessonCategories = [], +}: LessonCategoriesSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => lessonCategories.map((customer) => customer.id), [lessonCategories]); + const selection = useSelection(customerIds); + + return ( + + {children} + + ); +} + +export function useLessonCategoriesSelection(): LessonCategoriesSelectionContextValue { + return React.useContext(LessonCategoriesSelectionContext); +} diff --git a/002_source/cms/src/components/dashboard/vocabulary/lesson-categories-table.tsx b/002_source/cms/src/components/dashboard/vocabulary/lesson-categories-table.tsx new file mode 100644 index 0000000..f90667a --- /dev/null +++ b/002_source/cms/src/components/dashboard/vocabulary/lesson-categories-table.tsx @@ -0,0 +1,244 @@ +'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 Chip from '@mui/material/Chip'; +import IconButton from '@mui/material/IconButton'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; +import { 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 { useLessonCategoriesSelection } from './lesson-categories-selection-context'; +import type { LessonCategory } 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: LessonCategory[]; + reloadRows: () => void; +} + +export function LessonCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element { + const { t } = useTranslation(['lesson_category']); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLessonCategoriesSelection(); + + 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 ? ( + + + {/* TODO: use hyphen here */} + {t('no-lesson-categories-found')} + + + ) : null} + + ); +} diff --git a/002_source/cms/src/components/dashboard/vocabulary/lesson-category-create-form.tsx b/002_source/cms/src/components/dashboard/vocabulary/lesson-category-create-form.tsx new file mode 100644 index 0000000..7744b52 --- /dev/null +++ b/002_source/cms/src/components/dashboard/vocabulary/lesson-category-create-form.tsx @@ -0,0 +1,275 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { COL_LESSON_CATEGORIES, NS_LESSON_CATEGORY } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +import { Avatar, Divider, MenuItem } 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 Checkbox from '@mui/material/Checkbox'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +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 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 { logger } from '@/lib/default-logger'; +import { fileToBase64 } from '@/lib/file-to-base64'; +import { Option } from '@/components/core/option'; +import { TextEditor } from '@/components/core/text-editor/text-editor'; +import { toast } from '@/components/core/toaster'; + +const schema = zod.object({ + avatar: zod.string().optional(), + name: zod.string().min(1, 'Name is required').max(255), + email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255), + phone: zod.string().min(1, 'Phone is required').max(15), + company: zod.string().max(255), + billingAddress: zod.object({ + country: zod.string().min(1, 'Country is required').max(255), + state: zod.string().min(1, 'State is required').max(255), + city: zod.string().min(1, 'City is required').max(255), + zipCode: zod.string().min(1, 'Zip code is required').max(255), + line1: zod.string().min(1, 'Street line 1 is required').max(255), + line2: zod.string().max(255).optional(), + }), + taxId: zod.string().max(255).optional(), + timezone: zod.string().min(1, 'Timezone is required').max(255), + language: zod.string().min(1, 'Language is required').max(255), + currency: zod.string().min(1, 'Currency is required').max(255), +}); + +type Values = zod.infer; + +const defaultValues = { + avatar: '', + name: '', + email: '', + phone: '', + company: '', + billingAddress: { country: '', state: '', city: '', zipCode: '', line1: '', line2: '' }, + taxId: '', + timezone: 'new_york', + language: 'en', + currency: 'USD', +} satisfies Values; + +export function LessonCategoryCreateForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['common', 'lesson_category']); + + const NS_DEFAULT = { ns: 'lesson_category' }; + + 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); + try { + // Make API request + toast.success('Customer updated'); + router.push(paths.dashboard.lesson_categories.details('1')); + } catch (err) { + logger.error(err); + toast.error('Something went wrong!'); + } + }, + [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.typeInformation', NS_DEFAULT)} + + + + + + + + + + {t('create.avatar', NS_DEFAULT)} + {t('create.avatarRequirements', NS_DEFAULT)} + + + + + + + ( + + {t('create.name', NS_DEFAULT)} + + {errors.name ? {errors.name.message} : null} + + )} + /> + + + ( + + Email address + + {errors.email ? {errors.email.message} : null} + + )} + /> + + + ( + + Phone number + + {errors.phone ? {errors.phone.message} : null} + + )} + /> + + + ( + + Company + + {errors.company ? {errors.company.message} : null} + + )} + /> + + + + + {t('create.detail-information', NS_DEFAULT)} + + + ( + + + {t('create.description', NS_DEFAULT)} + + + + + + )} + /> + + + ( + + + {t('create.remarks', NS_DEFAULT)} + + + + + + )} + /> + + + + + + + + + + {t('create.createButton', NS_DEFAULT)} + + + +
+ ); +} diff --git a/002_source/cms/src/components/dashboard/vocabulary/lesson-category-edit-form.tsx b/002_source/cms/src/components/dashboard/vocabulary/lesson-category-edit-form.tsx new file mode 100644 index 0000000..2ec64e2 --- /dev/null +++ b/002_source/cms/src/components/dashboard/vocabulary/lesson-category-edit-form.tsx @@ -0,0 +1,353 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +import { COL_LESSON_CATEGORIES, NS_LESSON_CATEGORY } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +import { Avatar, Divider, MenuItem } 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 Checkbox from '@mui/material/Checkbox'; + +import FormControl from '@mui/material/FormControl'; +// import FormControlLabel from '@mui/material/FormControlLabel'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +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 type { RecordModel } from 'pocketbase'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { logger } from '@/lib/default-logger'; +import { fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { Option } from '@/components/core/option'; +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 { defaultLessonCategory } from './_constants'; +import type { EditFormProps, LessonCategory } from './type'; + +// TODO: review this +const schema = zod.object({ + cat_name: zod.string().min(1, 'name-is-required').max(255), + // + pos: zod.number().min(1, 'Phone is required').max(99), + visible: zod.string().max(255), + // + description: zod.string().optional(), + remarks: zod.string().optional(), +}); + +type Values = zod.infer; + +const defaultValues = { + cat_name: '', + // cat_image: undefined, + pos: 0, + visible: 'hidden', + // lesson_id: 'default-lesson-id', + description: 'default-description', + remarks: 'default-remarks', + // +} satisfies Values; + +export function LessonCategoryEditForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['common', 'lesson_category']); + + const NS_DEFAULT = { ns: 'lesson_category' }; + + 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(false); + + 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, + pos: values.pos, + visible: values.visible, + description: values.description, + remarks: values.remarks, + type: '', + }; + + pb.collection(COL_LESSON_CATEGORIES) + .update(catId, tempUpdate) + .then((res) => { + logger.debug(res); + toast.success(t('update.success', NS_DEFAULT)); + router.push(paths.dashboard.lesson_categories.list); + }) + .catch((err) => { + logger.error(err); + toast.error('Something went wrong!'); + }) + .finally(() => { + // + setIsUpdating(false); + }); + }, []); + + 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] + ); + + const [textDescription, setTextDescription] = React.useState('loading'); + const [textRemarks, setTextRemarks] = React.useState('loading'); + const handleLoad = React.useCallback( + (id: string) => { + setShowLoading(true); + pb.collection(COL_LESSON_CATEGORIES) + .getOne(id) + .then((model: RecordModel) => { + const temp: LessonCategory = { ...defaultLessonCategory, ...model }; + reset(temp); + setTextDescription(temp.description); + setTextRemarks(temp.remarks); + }) + .catch((err) => { + logger.error(err); + toast(t('list.error', NS_DEFAULT)); + }) + .finally(() => { + setShowLoading(false); + }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [catId] + ); + + React.useEffect(() => { + handleLoad(catId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [catId]); + + if (showLoading) return ; + if (showError) + return ( + + ); + + return ( +
+ + + } spacing={4}> + + {t('edit.basic-info', NS_DEFAULT)} + + + + + {/* + + // TODO: resume me + + + */} + + + {t('edit.avatar', NS_DEFAULT)} + {t('edit.avatarRequirements', NS_DEFAULT)} + + + + + + + ( + + {t('edit.name', NS_DEFAULT)} + + {errors.cat_name ? {errors.cat_name.message} : null} + + )} + /> + + + + ( + + {t('edit.position', NS_DEFAULT)} + { + field.onChange(parseInt(e.target.value)); + }} + type="number" + /> + + {errors.pos ? {errors.pos.message} : null} + + )} + /> + + + ( + + {t('edit.visible', NS_DEFAULT)} + + + {errors.visible ? {errors.visible.message} : null} + + )} + /> + + + + + + {t('create.detail-information', NS_DEFAULT)} + + + { + return ( + + + {t('create.description', NS_DEFAULT)} + + + { + field.onChange({ target: { value: editor.getHTML() } }); + }} + placeholder={t('edit.write-something', NS_DEFAULT)} + /> + + + ); + }} + /> + + + ( + + + {t('create.remarks', NS_DEFAULT)} + + + { + field.onChange({ target: { value: editor.getText() } }); + }} + hideToolbar + placeholder={t('edit.write-something', NS_DEFAULT)} + /> + + + )} + /> + + + + + + + + + + {t('edit.updateButton', NS_DEFAULT)} + + + +
+ ); +} diff --git a/002_source/cms/src/components/dashboard/vocabulary/notifications.tsx b/002_source/cms/src/components/dashboard/vocabulary/notifications.tsx new file mode 100644 index 0000000..a6c16bd --- /dev/null +++ b/002_source/cms/src/components/dashboard/vocabulary/notifications.tsx @@ -0,0 +1,101 @@ +'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/vocabulary/payments.tsx b/002_source/cms/src/components/dashboard/vocabulary/payments.tsx new file mode 100644 index 0000000..0420d32 --- /dev/null +++ b/002_source/cms/src/components/dashboard/vocabulary/payments.tsx @@ -0,0 +1,138 @@ +'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/vocabulary/shipping-address.tsx b/002_source/cms/src/components/dashboard/vocabulary/shipping-address.tsx new file mode 100644 index 0000000..8793e5c --- /dev/null +++ b/002_source/cms/src/components/dashboard/vocabulary/shipping-address.tsx @@ -0,0 +1,46 @@ +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/vocabulary/type.d.ts b/002_source/cms/src/components/dashboard/vocabulary/type.d.ts new file mode 100644 index 0000000..4ea1ce5 --- /dev/null +++ b/002_source/cms/src/components/dashboard/vocabulary/type.d.ts @@ -0,0 +1,47 @@ +export interface LessonCategory { + 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; + createdAt: Date; + // + name: string; + avatar: string; + email: string; + phone: string; + quota: number; + status: 'pending' | 'active' | 'blocked' | 'NA'; +} + +export interface CreateForm { + name: string; + type: string; + pos: number; + visible: string; + description: string; + isActive: boolean; + order: number; + imageUrl: string; +} + +export interface EditFormProps { + cat_name: string; + pos: number; + visible: string; + description?: string; + remarks?: string; + type: string; +} + +export interface Helloworld { + helloworld: string; +} diff --git a/002_source/cms/src/paths.ts b/002_source/cms/src/paths.ts index 89c8db7..43d71bf 100644 --- a/002_source/cms/src/paths.ts +++ b/002_source/cms/src/paths.ts @@ -88,11 +88,11 @@ export const paths = { details: (id: string) => `/dashboard/lesson_categories/view/${id}`, edit: (id: string) => `/dashboard/lesson_categories/edit/${id}`, }, - vocabulary: { - list: '/dashboard/vocabulary/list', - create: '/dashboard/vocabulary/create', - details: (id: string) => `/dashboard/vocabulary/view/${id}`, - edit: (id: string) => `/dashboard/vocabulary/edit/${id}`, + vocabularies: { + list: '/dashboard/vocabularies/list', + create: '/dashboard/vocabularies/create', + details: (id: string) => `/dashboard/vocabularies/view/${id}`, + edit: (id: string) => `/dashboard/vocabularies/edit/${id}`, }, connectives: { list: '/dashboard/connectives/list',