diff --git a/002_source/cms/.gitignore b/002_source/cms/.gitignore index a8e10fa..da9d21f 100644 --- a/002_source/cms/.gitignore +++ b/002_source/cms/.gitignore @@ -1,3 +1,8 @@ +**/*del +**/*bak +**/*log +**/*tmp + .env .env.production diff --git a/002_source/cms/TODO.md b/002_source/cms/TODO.md index 19db964..7ca214d 100644 --- a/002_source/cms/TODO.md +++ b/002_source/cms/TODO.md @@ -1,9 +1,12 @@ +# TODO + need to fix local storage error page right (next page) button is not working in the middle of clone lesson type to lesson category - - [ ] listing page - - [ ] delete button on each row - - [ ] create page - - [ ] edit - - [ ] delete + +- [ ] listing page + - [ ] delete button on each row +- [ ] create page +- [ ] edit +- [ ] delete diff --git a/002_source/cms/_AI_WORKSPACE_obsoleted/_examples/001_clone.md b/002_source/cms/_AI_WORKSPACE_obsoleted/_examples/001_clone.md new file mode 100644 index 0000000..f508e25 --- /dev/null +++ b/002_source/cms/_AI_WORKSPACE_obsoleted/_examples/001_clone.md @@ -0,0 +1,18 @@ +please review and update all tsx files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Users` to make it handle `user` record thanks + +--- + +please modify and update `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/students/edit/[customerId]/page.tsx.draft` to handle `Student` record thanks, +modify comments/variables/paths/functions name please + +--- + +please help to update the tsx files inside folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/student` to handle the `student` record + +## steps + +- list all `tsx` files inside directory, remember the list +- clone the original `.tsx` files to `.tsx.draft` +- do all your modification within `.tsx.draft` files, leave `original.tsx` unchange + +--- diff --git a/002_source/cms/_AI_WORKSPACE_obsoleted/_examples/003_update_dbml_from_schema.md b/002_source/cms/_AI_WORKSPACE_obsoleted/_examples/003_update_dbml_from_schema.md new file mode 100644 index 0000000..c75e8e2 --- /dev/null +++ b/002_source/cms/_AI_WORKSPACE_obsoleted/_examples/003_update_dbml_from_schema.md @@ -0,0 +1,33 @@ +Hi, i need your help. + +## task + +i am working on a `dbml` file +i got a `schema.json` which is exported from pocketbase +and i want to update it to my current `dbml` file (one way process for documentation usage) + +## Rules + +- the collection from `json` file started with `_` can be ignored. they are system collection and should not appear in `dbml` +- one collection from `json` file mapped with one table in `dbml` file +- the `presentable` field from `json` file should be ignored. +- the `id` of collection in `json` file should be jod down in the comment of `dbml` file as an reference. +- you can find the comments in `schema.dbml` contains `pb_xxx` and that is the reference to the table in `schema.json` file. + +## steps + +- list the collection + +## information + +json file: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.json` +dbml file: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml` + +## FAQ + +1. 对于json中有但dbml中没有的表,应该如何处理? 添加为新表 +1. 是否需要保留dbml文件中现有的注释和关系定义? 完全保留 +1. 字段类型映射是否有特殊规则? 沒有 +1. please keep the existing comment + +thanks diff --git a/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/0010_FAQ.md b/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/0010_FAQ.md new file mode 100644 index 0000000..78ce37a --- /dev/null +++ b/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/0010_FAQ.md @@ -0,0 +1,7 @@ +# FAQ + +Q: where is `dbml` file ? +A: dbml file located in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml` + +Q: when file not found, do i need to search it in `_ignore_this_directory` ? +A: No, you just stop there and voice out. diff --git a/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/001_greetings.md b/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/001_greetings.md new file mode 100644 index 0000000..21dcb1f --- /dev/null +++ b/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/001_greetings.md @@ -0,0 +1,8 @@ +Hi, i will send you the guideline, +plesae read it, prepare yourself and i will tell you the task afterwards + +please read and understand the markdown files in directory +`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/_AI_WORKSPACE/greetings`, +it provides background information of project i want you to help. + +thanks diff --git a/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/002_guideline.md b/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/002_guideline.md new file mode 100644 index 0000000..6a94824 --- /dev/null +++ b/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/002_guideline.md @@ -0,0 +1,28 @@ +# guideline + +## principles + +- at any time, please keep your answer, solution, explaination simple and short (K.I.S.S. or 大道至簡) +- please divide the problem into small parts +- if you found youself cannot understand the problem, please stop and ask how to do +- if you found youself cannot solve the problem, plesae stop and ask how to do +- review the whole solution before you reply to user +- if code syntax is already there, do follow (e.g. naming convention, syntax) the existing code +- no need to explain the reason until you are told to do so +- no need to show me the code change, at the end just simple summary in point form is ok + +## highlighted project directories and their meanings + +- `_ignore_this_directory` please ignore this directory and any files inside it + +- `001_documentation` documentation of this project +- `002_source` source code of this project +- `002_source/cms` home of Context management system of this project +- `002_source/ionic` home of mobile client of this project +- `002_source/pocketbase` home of pocketbase home directory this project +- `003_test` e2e test of this project (not yet implemented) +- `004_marketing` marketing page of this project (not yet implemented) +- `005_references` opensource refence of this project +- `006_lab` my test (POC) of this project +- `README.md` Readme of this project +- `TODO.md` todo list of this project diff --git a/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/003_knowledgebase.md b/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/003_knowledgebase.md new file mode 100644 index 0000000..ad12950 --- /dev/null +++ b/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/003_knowledgebase.md @@ -0,0 +1,12 @@ +# Knowledgebase + +you can answer the question with below knowledge: + +## frameworks and stacks + +- if code syntax is already there, do follow (e.g. naming convention, syntax) the existing code +- make use of MCP `Context7` when you troubleshoot the problem with below topics: + - [pocketbase javascript SDK](https://context7.com/pocketbase/js-sdk/llms.txt) + - [DBML](https://context7.com/holistics/dbml/llms.txt) + - [ionic framework](https://context7.com/ionic-team/ionic-framework/llms.txt) + - [nextjs 14 app router](https://context7.com/nextjsargentina/next.js-docs/llms.txt) diff --git a/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/013_DB.md b/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/013_DB.md new file mode 100644 index 0000000..1b15799 --- /dev/null +++ b/002_source/cms/_AI_WORKSPACE_obsoleted/greetings/013_DB.md @@ -0,0 +1,48 @@ +# AI GUIDELINE + +## getting started + +Imagine there is a: + +1. developer (provide the modification) +2. QA engineer (provide the feedback, and testing) +3. software engineer +4. technical writer + +they will: + +- conclude and integrate the ideas from developer and QA engineer +- make decision to modify the code accordingly. + +## project background and initial setup + +- **IMPORTANT**: No need to reply me what you are going on and your digest in this phase. + No need to show me your code plan + Just reply me "OK" when done + +- base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project` + +- `schema.dbml` + - read `/001_documentation/Requirements/REQ0006/schema.dbml` + this is file in `dbml` format stating the main database structure + +- `schema.json` + - read `/002_source/cms/src/db/schema.json` + this is the file of current pocketbase schema + +- look into the md files in folder `/002_source/ionic_mobile/_AI_WORKSPACE/001_guideline` + +- if the directory user provided contins `_GUIDELINES.md`, please read the file + +- read the files, remember and link up the ideas in file stated above, i will tell them the task afterwards + +- please review at least 3 times after you modified the code + +## frameworks documentation and samples + +- react +- ionic and capacitor +- pocketbase +- tanstack/react-query +- vite +- typescript diff --git a/002_source/cms/cms-letter_soup.code-workspace b/002_source/cms/cms-letter_soup.code-workspace new file mode 100644 index 0000000..7ef4683 --- /dev/null +++ b/002_source/cms/cms-letter_soup.code-workspace @@ -0,0 +1,14 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "./../../001_documentation" + }, + { + "path": "../../000_AI_WORKSPACE" + } + ], + "settings": {} +} diff --git a/002_source/cms/package.json b/002_source/cms/package.json index 94e2dbb..8f01c52 100644 --- a/002_source/cms/package.json +++ b/002_source/cms/package.json @@ -4,7 +4,7 @@ "version": "7.0.0", "private": true, "engines": { - "node": ">=18" + "node": "==22" }, "scripts": { "dev": "next dev", diff --git a/002_source/cms/public/locales/dev/common.json b/002_source/cms/public/locales/dev/common.json index 0967ef4..eeedd40 100644 --- a/002_source/cms/public/locales/dev/common.json +++ b/002_source/cms/public/locales/dev/common.json @@ -1 +1,3 @@ -{} +{ + "hello": "world" +} \ No newline at end of file diff --git a/002_source/cms/scripts/004_dev.sh b/002_source/cms/scripts/004_dev.sh index 613ce9a..4d88bda 100755 --- a/002_source/cms/scripts/004_dev.sh +++ b/002_source/cms/scripts/004_dev.sh @@ -2,5 +2,4 @@ set -ex -rm -rf .next pnpm run dev diff --git a/002_source/cms/src/app/dashboard/mf/_repomix.md b/002_source/cms/src/app/dashboard/mf/_repomix.md deleted file mode 100644 index 7e31251..0000000 --- a/002_source/cms/src/app/dashboard/mf/_repomix.md +++ /dev/null @@ -1,3336 +0,0 @@ -This file is a merged representation of the entire codebase, combined into a single document by Repomix. - -# File Summary - -## Purpose -This file contains a packed representation of the entire repository's contents. -It is designed to be easily consumable by AI systems for analysis, code review, -or other automated processes. - -## File Format -The content is organized as follows: -1. This summary section -2. Repository information -3. Directory structure -4. Multiple file entries, each consisting of: - a. A header with the file path (## File: path/to/file) - b. The full contents of the file in a code block - -## Usage Guidelines -- This file should be treated as read-only. Any changes should be made to the - original repository files, not this packed version. -- When processing this file, use the file path to distinguish - between different files in the repository. -- Be aware that this file may contain sensitive information. Handle it with - the same level of security as you would the original repository. - -## Notes -- Some files may have been excluded based on .gitignore rules and Repomix's configuration -- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files -- Files matching patterns in .gitignore are excluded -- Files matching default ignore patterns are excluded -- Files are sorted by Git change count (files with more changes are at the bottom) - -## Additional Info - -# Directory Structure -``` -categories/ - [cat_id]/ - BasicDetailCard.tsx - page.tsx - TitleCard.tsx - create/ - page.tsx - edit/ - [cat_id]/ - _PROMPT.md - page.tsx - lp-categories-sample-data.tsx - page.tsx -questions/ - [cat_id]/ - BasicDetailCard.tsx - page.tsx - TitleCard.tsx - create/ - page.tsx - edit/ - [cat_id]/ - _PROMPT.md - page.tsx - lp-categories-sample-data.tsx - page.tsx -repomix-output.xml -``` - -# Files - -## File: categories/[cat_id]/BasicDetailCard.tsx -```typescript -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Card from '@mui/material/Card'; -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 { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; -import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; -import { useTranslation } from 'react-i18next'; - -import { PropertyItem } from '@/components/core/property-item'; -import { PropertyList } from '@/components/core/property-list'; -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; - -export default function BasicDetailCard({ - lpModel: model, - handleEditClick, -}: { - lpModel: MfCategory; - handleEditClick: () => void; -}): React.JSX.Element { - const { t } = useTranslation(); - - return ( - - { - handleEditClick(); - }} - > - - - } - avatar={ - - - - } - title={t('list.basic-details')} - /> - } - orientation="vertical" - sx={{ '--PropertyItem-padding': '12px 24px' }} - > - {( - [ - { - key: 'Customer ID', - value: ( - - ), - }, - { key: 'Name', value: model.cat_name }, - { key: 'Remarks', value: model.remarks }, - { key: 'Description', value: model.description }, - ] satisfies { key: string; value: React.ReactNode }[] - ).map( - (item): React.JSX.Element => ( - - ) - )} - - - ); -} -``` - -## File: categories/[cat_id]/page.tsx -```typescript -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; -import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; -import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; -import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; -import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Grid from '@mui/material/Unstable_Grid2'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import type { RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { pb } from '@/lib/pb'; -import { toast } from '@/components/core/toaster'; -import ErrorDisplay from '@/components/dashboard/error'; -import { defaultMfCategory } from '@/components/dashboard/mf/categories/_constants.ts'; -import { Notifications } from '@/components/dashboard/mf/categories/notifications'; -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; -import FormLoading from '@/components/loading'; - -import BasicDetailCard from './BasicDetailCard'; -import TitleCard from './TitleCard'; - -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({ show: false, detail: '' }); - - // - const [showLessonCategory, setShowLessonCategory] = React.useState(defaultMfCategory); - - function handleEditClick() { - router.push(paths.dashboard.mf_categories.edit(showLessonCategory.id)); - } - - React.useEffect(() => { - if (catId) { - pb.collection(COL_QUIZ_MF_CATEGORIES) - .getOne(catId) - .then((model: RecordModel) => { - setShowLessonCategory({ ...defaultMfCategory, ...model }); - }) - .catch((err) => { - logger.error(err); - toast(t('list.error')); - - setShowError({ show: true, detail: JSON.stringify(err) }); - }) - .finally(() => { - setShowLoading(false); - }); - } - }, [catId]); - - if (showLoading) return ; - if (showError.show) - return ( - - ); - - return ( - - - -
- - - {t('list.title')} - -
- - - -
- - - - - - - - - - - - - - - -
-
- ); -} -``` - -## File: categories/[cat_id]/TitleCard.tsx -```typescript -'use client'; - -import * as React from 'react'; -import { Button } from '@mui/material'; -import Avatar from '@mui/material/Avatar'; -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; -import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { useTranslation } from 'react-i18next'; - -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; - -function getImageUrlFrRecord(record: MfCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} - -export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element { - const { t } = useTranslation(); - - return ( - <> - - - {t('empty')} - -
- - {lpModel.cat_name} - - } - label={lpModel.visible} - size="small" - variant="outlined" - /> - - - {lpModel.slug} - -
-
-
- -
- - ); -} -``` - -## File: categories/create/page.tsx -```typescript -'use client'; - -// RULES: -// T.B.A. -// -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 { MfCategoryCreateForm } from '@/components/dashboard/mf/categories/mf-category-create-form'; - -export default function Page(): React.JSX.Element { - // RULES: follow the name of page directory - const { t } = useTranslation(['lp_categories']); - - return ( - - - -
- - - {t('title')} - -
-
- {t('create.title')} -
-
- -
-
- ); -} -``` - -## File: categories/edit/[cat_id]/_PROMPT.md -```markdown -# 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, -``` - -## File: categories/edit/[cat_id]/page.tsx -```typescript -'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 { MfCategoryEditForm } from '@/components/dashboard/mf/categories/mf-category-edit-form'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(['lp_categories']); - - React.useEffect(() => { - // console.log('helloworld'); - }, []); - - return ( - - - -
- - - {t('edit.title')} - -
-
- {t('edit.title')} -
-
- -
-
- ); -} -``` - -## File: categories/lp-categories-sample-data.tsx -```typescript -import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; - -export const LpCategoriesSampleData = [ - { - 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[]; -``` - -## File: categories/page.tsx -```typescript -'use client'; - -// RULES: -// contains list page for lp_categories (QuizLPCategories) -// contain definition to collection only -// -import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import { COL_QUIZ_MF_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 { defaultMfCategory } from '@/components/dashboard/mf/categories/_constants'; -import { MfCategoriesFilters } from '@/components/dashboard/mf/categories/mf-categories-filters'; -import type { Filters } from '@/components/dashboard/mf/categories/mf-categories-filters'; -import { MfCategoriesPagination } from '@/components/dashboard/mf/categories/mf-categories-pagination'; -import { MfCategoriesSelectionProvider } from '@/components/dashboard/mf/categories/mf-categories-selection-context'; -import { MfCategoriesTable } from '@/components/dashboard/mf/categories/mf-categories-table'; -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; -import FormLoading from '@/components/loading'; - -export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { t } = useTranslation(['mf_categories']); - 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_QUIZ_MF_CATEGORIES) - .getList(currentPage + 1, rowsPerPage, listOption); - const { items, totalItems } = models; - const tempLessonTypes: MfCategory[] = items.map((lt) => { - return { ...defaultMfCategory, ...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); - } - }; - - 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.mf_categories.create); - }} - startIcon={} - variant="contained" - > - {t('list.add')} - - - - - - - - - - - - - - - - -
{JSON.stringify(f, null, 2)}
-
-
- ); -} - -// Sorting and filtering has to be done on the server. - -function applySort(row: MfCategory[], sortDir: 'asc' | 'desc' | undefined): MfCategory[] { - 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: MfCategory[], { email, phone, status, name, visible }: Filters): MfCategory[] { - 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; - // - }; -} -``` - -## File: questions/[cat_id]/BasicDetailCard.tsx -```typescript -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Card from '@mui/material/Card'; -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 { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; -import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; -import { useTranslation } from 'react-i18next'; - -import { PropertyItem } from '@/components/core/property-item'; -import { PropertyList } from '@/components/core/property-list'; -import { MfCategory } from '@/components/dashboard/mf/categories/type'; - -export default function BasicDetailCard({ - lpModel: model, - handleEditClick, -}: { - lpModel: MfCategory; - handleEditClick: () => void; -}): React.JSX.Element { - const { t } = useTranslation(); - - return ( - - { - handleEditClick(); - }} - > - - - } - avatar={ - - - - } - title={t('list.basic-details')} - /> - } - orientation="vertical" - sx={{ '--PropertyItem-padding': '12px 24px' }} - > - {( - [ - { - key: 'Customer ID', - value: ( - - ), - }, - { key: 'Name', value: model.cat_name }, - { key: 'Remarks', value: model.remarks }, - { key: 'Description', value: model.description }, - ] satisfies { key: string; value: React.ReactNode }[] - ).map( - (item): React.JSX.Element => ( - - ) - )} - - - ); -} -``` - -## File: questions/[cat_id]/page.tsx -```typescript -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; -import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; -import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; -import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; -import { COL_QUIZ_MF_QUESTIONS } from '@/constants'; -import { Grid } from '@mui/material'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import type { RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { pb } from '@/lib/pb'; -import { toast } from '@/components/core/toaster'; -import ErrorDisplay from '@/components/dashboard/error'; -import { defaultMfQuestion } from '@/components/dashboard/mf/questions/_constants.ts'; -import { Notifications } from '@/components/dashboard/mf/questions/notifications'; -import type { MfQuestion } from '@/components/dashboard/mf/questions/type'; -import FormLoading from '@/components/loading'; - -import BasicDetailCard from './BasicDetailCard'; -import TitleCard from './TitleCard'; - -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({ show: false, detail: '' }); - - // - const [showLessonQuestion, setShowLessonQuestion] = React.useState(defaultMfQuestion); - - function handleEditClick() { - router.push(paths.dashboard.mf_questions.edit(showLessonQuestion.id)); - } - - React.useEffect(() => { - if (catId) { - pb.collection(COL_QUIZ_MF_QUESTIONS) - .getOne(catId) - .then((model: RecordModel) => { - setShowLessonQuestion({ ...defaultMfQuestion, ...model }); - }) - .catch((err) => { - logger.error(err); - toast(t('list.error')); - - setShowError({ show: true, detail: JSON.stringify(err) }); - }) - .finally(() => { - setShowLoading(false); - }); - } - }, [catId]); - - if (showLoading) return ; - if (showError.show) - return ( - - ); - - return ( - - - -
- - - {t('edit.title')} - -
- - - -
- - - - - - - - - - - - - - - -
-
- ); -} -``` - -## File: questions/[cat_id]/TitleCard.tsx -```typescript -'use client'; - -import * as React from 'react'; -import { Button } from '@mui/material'; -import Avatar from '@mui/material/Avatar'; -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; -import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { useTranslation } from 'react-i18next'; - -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; - -function getImageUrlFrRecord(record: MfCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} - -export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element { - const { t } = useTranslation(); - - return ( - <> - - - {t('empty')} - -
- - {lpModel.cat_name} - - } - label={lpModel.visible} - size="small" - variant="outlined" - /> - - - {lpModel.slug} - -
-
-
- -
- - ); -} -``` - -## File: questions/create/page.tsx -```typescript -'use client'; - -// RULES: -// T.B.A. -// -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 { MfQuestionCreateForm } from '@/components/dashboard/mf/questions/mf-question-create-form'; - -export default function Page(): React.JSX.Element { - // RULES: follow the name of page directory - const { t } = useTranslation(['lp_questions']); - - return ( - - - -
- - - {t('title')} - -
-
- {t('create.title')} -
-
- -
-
- ); -} -``` - -## File: questions/edit/[cat_id]/_PROMPT.md -```markdown -# 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, -``` - -## File: questions/edit/[cat_id]/page.tsx -```typescript -'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 { MfQuestionEditForm } from '@/components/dashboard/mf/questions/mf-question-edit-form'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(['lp_questions']); - - React.useEffect(() => { - // console.log('helloworld'); - }, []); - - return ( - - - -
- - - {t('edit.title')} - -
-
- {t('edit.title')} -
-
- -
-
- ); -} -``` - -## File: questions/lp-categories-sample-data.tsx -```typescript -import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; - -export const LpCategoriesSampleData = [ - { - 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[]; -``` - -## File: questions/page.tsx -```typescript -'use client'; - -// RULES: -// contains list page for lp_questions (QuizLPQuestions) -// contain definition to collection only -// -import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import { COL_QUIZ_MF_QUESTIONS } 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 { defaultMfQuestion } from '@/components/dashboard/mf/questions/_constants'; -import { MfQuestionsFilters } from '@/components/dashboard/mf/questions/mf-questions-filters'; -import type { Filters } from '@/components/dashboard/mf/questions/mf-questions-filters'; -import { MfQuestionsPagination } from '@/components/dashboard/mf/questions/mf-questions-pagination'; -import { MfQuestionsSelectionProvider } from '@/components/dashboard/mf/questions/mf-questions-selection-context'; -import { MfQuestionsTable } from '@/components/dashboard/mf/questions/mf-questions-table'; -import type { MfQuestion } from '@/components/dashboard/mf/questions/type'; -import FormLoading from '@/components/loading'; - -export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { t } = useTranslation(['mf_question']); - const { email, phone, sortDir, status, name, visible, type } = searchParams; - const router = useRouter(); - const [lessonQuestionsData, 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(0); - const [recordCount, setRecordCount] = React.useState(0); - const [listOption, setListOption] = React.useState({}); - const [listSort, setListSort] = React.useState({}); - - // - const sortedLessonCategories = applySort(lessonQuestionsData, sortDir); - const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status }); - - const reloadRows = async (): Promise => { - try { - const models: ListResult = await pb - .collection(COL_QUIZ_MF_QUESTIONS) - .getList(currentPage + 1, rowsPerPage, listOption); - const { items, totalItems } = models; - const tempLessonTypes: MfQuestion[] = items.map((lt) => { - return { ...defaultMfQuestion, ...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); - } - }; - - 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.lp_questions.create); - }} - startIcon={} - variant="contained" - > - {t('list.add')} - - - - - - - - - - - - - - - - -
{JSON.stringify(f, null, 2)}
-
-
- ); -} - -// Sorting and filtering has to be done on the server. - -function applySort(row: MfQuestion[], sortDir: 'asc' | 'desc' | undefined): MfQuestion[] { - 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: MfQuestion[], { email, phone, status, name, visible }: Filters): MfQuestion[] { - 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; - // - }; -} -``` - -## File: repomix-output.xml -```xml -This file is a merged representation of the entire codebase, combined into a single document by Repomix. - - -This section contains a summary of this file. - - -This file contains a packed representation of the entire repository's contents. -It is designed to be easily consumable by AI systems for analysis, code review, -or other automated processes. - - - -The content is organized as follows: -1. This summary section -2. Repository information -3. Directory structure -4. Repository files, each consisting of: - - File path as an attribute - - Full contents of the file - - - -- This file should be treated as read-only. Any changes should be made to the - original repository files, not this packed version. -- When processing this file, use the file path to distinguish - between different files in the repository. -- Be aware that this file may contain sensitive information. Handle it with - the same level of security as you would the original repository. - - - -- Some files may have been excluded based on .gitignore rules and Repomix's configuration -- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files -- Files matching patterns in .gitignore are excluded -- Files matching default ignore patterns are excluded -- Files are sorted by Git change count (files with more changes are at the bottom) - - - - - - - - - -categories/ - [cat_id]/ - BasicDetailCard.tsx - page.tsx - TitleCard.tsx - create/ - page.tsx - edit/ - [cat_id]/ - _PROMPT.md - page.tsx - lp-categories-sample-data.tsx - page.tsx -questions/ - [cat_id]/ - BasicDetailCard.tsx - page.tsx - TitleCard.tsx - create/ - page.tsx - edit/ - [cat_id]/ - _PROMPT.md - page.tsx - lp-categories-sample-data.tsx - page.tsx - - - -This section contains the contents of the repository's files. - - -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Card from '@mui/material/Card'; -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 { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; -import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; -import { useTranslation } from 'react-i18next'; - -import { PropertyItem } from '@/components/core/property-item'; -import { PropertyList } from '@/components/core/property-list'; -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; - -export default function BasicDetailCard({ - lpModel: model, - handleEditClick, -}: { - lpModel: MfCategory; - handleEditClick: () => void; -}): React.JSX.Element { - const { t } = useTranslation(); - - return ( - - { - handleEditClick(); - }} - > - - - } - avatar={ - - - - } - title={t('list.basic-details')} - /> - } - orientation="vertical" - sx={{ '--PropertyItem-padding': '12px 24px' }} - > - {( - [ - { - key: 'Customer ID', - value: ( - - ), - }, - { key: 'Name', value: model.cat_name }, - { key: 'Remarks', value: model.remarks }, - { key: 'Description', value: model.description }, - ] satisfies { key: string; value: React.ReactNode }[] - ).map( - (item): React.JSX.Element => ( - - ) - )} - - - ); -} - - - -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; -import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; -import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; -import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; -import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Grid from '@mui/material/Unstable_Grid2'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import type { RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { pb } from '@/lib/pb'; -import { toast } from '@/components/core/toaster'; -import ErrorDisplay from '@/components/dashboard/error'; -import { defaultMfCategory } from '@/components/dashboard/mf/categories/_constants.ts'; -import { Notifications } from '@/components/dashboard/mf/categories/notifications'; -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; -import FormLoading from '@/components/loading'; - -import BasicDetailCard from './BasicDetailCard'; -import TitleCard from './TitleCard'; - -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({ show: false, detail: '' }); - - // - const [showLessonCategory, setShowLessonCategory] = React.useState(defaultMfCategory); - - function handleEditClick() { - router.push(paths.dashboard.mf_categories.edit(showLessonCategory.id)); - } - - React.useEffect(() => { - if (catId) { - pb.collection(COL_QUIZ_MF_CATEGORIES) - .getOne(catId) - .then((model: RecordModel) => { - setShowLessonCategory({ ...defaultMfCategory, ...model }); - }) - .catch((err) => { - logger.error(err); - toast(t('list.error')); - - setShowError({ show: true, detail: JSON.stringify(err) }); - }) - .finally(() => { - setShowLoading(false); - }); - } - }, [catId]); - - if (showLoading) return ; - if (showError.show) - return ( - - ); - - return ( - - - -
- - - {t('list.title')} - -
- - - -
- - - - - - - - - - - - - - - -
-
- ); -} -
- - -'use client'; - -import * as React from 'react'; -import { Button } from '@mui/material'; -import Avatar from '@mui/material/Avatar'; -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; -import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { useTranslation } from 'react-i18next'; - -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; - -function getImageUrlFrRecord(record: MfCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} - -export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element { - const { t } = useTranslation(); - - return ( - <> - - - {t('empty')} - -
- - {lpModel.cat_name} - - } - label={lpModel.visible} - size="small" - variant="outlined" - /> - - - {lpModel.slug} - -
-
-
- -
- - ); -} -
- - -'use client'; - -// RULES: -// T.B.A. -// -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 { MfCategoryCreateForm } from '@/components/dashboard/mf/categories/mf-category-create-form'; - -export default function Page(): React.JSX.Element { - // RULES: follow the name of page directory - const { t } = useTranslation(['lp_categories']); - - return ( - - - -
- - - {t('title')} - -
-
- {t('create.title')} -
-
- -
-
- ); -} -
- - -# 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, - - - -'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 { MfCategoryEditForm } from '@/components/dashboard/mf/categories/mf-category-edit-form'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(['lp_categories']); - - React.useEffect(() => { - // console.log('helloworld'); - }, []); - - return ( - - - -
- - - {t('edit.title')} - -
-
- {t('edit.title')} -
-
- -
-
- ); -} -
- - -import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; - -export const LpCategoriesSampleData = [ - { - 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[]; - - - -'use client'; - -// RULES: -// contains list page for lp_categories (QuizLPCategories) -// contain definition to collection only -// -import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import { COL_QUIZ_MF_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 { defaultMfCategory } from '@/components/dashboard/mf/categories/_constants'; -import { MfCategoriesFilters } from '@/components/dashboard/mf/categories/mf-categories-filters'; -import type { Filters } from '@/components/dashboard/mf/categories/mf-categories-filters'; -import { MfCategoriesPagination } from '@/components/dashboard/mf/categories/mf-categories-pagination'; -import { MfCategoriesSelectionProvider } from '@/components/dashboard/mf/categories/mf-categories-selection-context'; -import { MfCategoriesTable } from '@/components/dashboard/mf/categories/mf-categories-table'; -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; -import FormLoading from '@/components/loading'; - -export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { t } = useTranslation(['mf_categories']); - 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_QUIZ_MF_CATEGORIES) - .getList(currentPage + 1, rowsPerPage, listOption); - const { items, totalItems } = models; - const tempLessonTypes: MfCategory[] = items.map((lt) => { - return { ...defaultMfCategory, ...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); - } - }; - - 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.mf_categories.create); - }} - startIcon={} - variant="contained" - > - {t('list.add')} - - - - - - - - - - - - - - - - -
{JSON.stringify(f, null, 2)}
-
-
- ); -} - -// Sorting and filtering has to be done on the server. - -function applySort(row: MfCategory[], sortDir: 'asc' | 'desc' | undefined): MfCategory[] { - 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: MfCategory[], { email, phone, status, name, visible }: Filters): MfCategory[] { - 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; - // - }; -} -
- - -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Card from '@mui/material/Card'; -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 { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; -import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; -import { useTranslation } from 'react-i18next'; - -import { PropertyItem } from '@/components/core/property-item'; -import { PropertyList } from '@/components/core/property-list'; -import { MfCategory } from '@/components/dashboard/mf/categories/type'; - -export default function BasicDetailCard({ - lpModel: model, - handleEditClick, -}: { - lpModel: MfCategory; - handleEditClick: () => void; -}): React.JSX.Element { - const { t } = useTranslation(); - - return ( - - { - handleEditClick(); - }} - > - - - } - avatar={ - - - - } - title={t('list.basic-details')} - /> - } - orientation="vertical" - sx={{ '--PropertyItem-padding': '12px 24px' }} - > - {( - [ - { - key: 'Customer ID', - value: ( - - ), - }, - { key: 'Name', value: model.cat_name }, - { key: 'Remarks', value: model.remarks }, - { key: 'Description', value: model.description }, - ] satisfies { key: string; value: React.ReactNode }[] - ).map( - (item): React.JSX.Element => ( - - ) - )} - - - ); -} - - - -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; -import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; -import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; -import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; -import { COL_QUIZ_MF_QUESTIONS } from '@/constants'; -import { Grid } from '@mui/material'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import type { RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { pb } from '@/lib/pb'; -import { toast } from '@/components/core/toaster'; -import ErrorDisplay from '@/components/dashboard/error'; -import { defaultMfQuestion } from '@/components/dashboard/mf/questions/_constants.ts'; -import { Notifications } from '@/components/dashboard/mf/questions/notifications'; -import type { MfQuestion } from '@/components/dashboard/mf/questions/type'; -import FormLoading from '@/components/loading'; - -import BasicDetailCard from './BasicDetailCard'; -import TitleCard from './TitleCard'; - -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({ show: false, detail: '' }); - - // - const [showLessonQuestion, setShowLessonQuestion] = React.useState(defaultMfQuestion); - - function handleEditClick() { - router.push(paths.dashboard.mf_questions.edit(showLessonQuestion.id)); - } - - React.useEffect(() => { - if (catId) { - pb.collection(COL_QUIZ_MF_QUESTIONS) - .getOne(catId) - .then((model: RecordModel) => { - setShowLessonQuestion({ ...defaultMfQuestion, ...model }); - }) - .catch((err) => { - logger.error(err); - toast(t('list.error')); - - setShowError({ show: true, detail: JSON.stringify(err) }); - }) - .finally(() => { - setShowLoading(false); - }); - } - }, [catId]); - - if (showLoading) return ; - if (showError.show) - return ( - - ); - - return ( - - - -
- - - {t('edit.title')} - -
- - - -
- - - - - - - - - - - - - - - -
-
- ); -} -
- - -'use client'; - -import * as React from 'react'; -import { Button } from '@mui/material'; -import Avatar from '@mui/material/Avatar'; -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; -import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { useTranslation } from 'react-i18next'; - -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; - -function getImageUrlFrRecord(record: MfCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} - -export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element { - const { t } = useTranslation(); - - return ( - <> - - - {t('empty')} - -
- - {lpModel.cat_name} - - } - label={lpModel.visible} - size="small" - variant="outlined" - /> - - - {lpModel.slug} - -
-
-
- -
- - ); -} -
- - -'use client'; - -// RULES: -// T.B.A. -// -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 { MfQuestionCreateForm } from '@/components/dashboard/mf/questions/mf-question-create-form'; - -export default function Page(): React.JSX.Element { - // RULES: follow the name of page directory - const { t } = useTranslation(['lp_questions']); - - return ( - - - -
- - - {t('title')} - -
-
- {t('create.title')} -
-
- -
-
- ); -} -
- - -# 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, - - - -'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 { MfQuestionEditForm } from '@/components/dashboard/mf/questions/mf-question-edit-form'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(['lp_questions']); - - React.useEffect(() => { - // console.log('helloworld'); - }, []); - - return ( - - - -
- - - {t('edit.title')} - -
-
- {t('edit.title')} -
-
- -
-
- ); -} -
- - -import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; - -export const LpCategoriesSampleData = [ - { - 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[]; - - - -'use client'; - -// RULES: -// contains list page for lp_questions (QuizLPQuestions) -// contain definition to collection only -// -import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import { COL_QUIZ_MF_QUESTIONS } 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 { defaultMfQuestion } from '@/components/dashboard/mf/questions/_constants'; -import { MfQuestionsFilters } from '@/components/dashboard/mf/questions/mf-questions-filters'; -import type { Filters } from '@/components/dashboard/mf/questions/mf-questions-filters'; -import { MfQuestionsPagination } from '@/components/dashboard/mf/questions/mf-questions-pagination'; -import { MfQuestionsSelectionProvider } from '@/components/dashboard/mf/questions/mf-questions-selection-context'; -import { MfQuestionsTable } from '@/components/dashboard/mf/questions/mf-questions-table'; -import type { MfQuestion } from '@/components/dashboard/mf/questions/type'; -import FormLoading from '@/components/loading'; - -export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { t } = useTranslation(['mf_question']); - const { email, phone, sortDir, status, name, visible, type } = searchParams; - const router = useRouter(); - const [lessonQuestionsData, 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(0); - const [recordCount, setRecordCount] = React.useState(0); - const [listOption, setListOption] = React.useState({}); - const [listSort, setListSort] = React.useState({}); - - // - const sortedLessonCategories = applySort(lessonQuestionsData, sortDir); - const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status }); - - const reloadRows = async (): Promise => { - try { - const models: ListResult = await pb - .collection(COL_QUIZ_MF_QUESTIONS) - .getList(currentPage + 1, rowsPerPage, listOption); - const { items, totalItems } = models; - const tempLessonTypes: MfQuestion[] = items.map((lt) => { - return { ...defaultMfQuestion, ...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); - } - }; - - 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.lp_questions.create); - }} - startIcon={} - variant="contained" - > - {t('list.add')} - - - - - - - - - - - - - - - - -
{JSON.stringify(f, null, 2)}
-
-
- ); -} - -// Sorting and filtering has to be done on the server. - -function applySort(row: MfQuestion[], sortDir: 'asc' | 'desc' | undefined): MfQuestion[] { - 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: MfQuestion[], { email, phone, status, name, visible }: Filters): MfQuestion[] { - 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/mf/repomix-output.xml b/002_source/cms/src/app/dashboard/mf/repomix-output.xml deleted file mode 100644 index 984f0a9..0000000 --- a/002_source/cms/src/app/dashboard/mf/repomix-output.xml +++ /dev/null @@ -1,1663 +0,0 @@ -This file is a merged representation of the entire codebase, combined into a single document by Repomix. - - -This section contains a summary of this file. - - -This file contains a packed representation of the entire repository's contents. -It is designed to be easily consumable by AI systems for analysis, code review, -or other automated processes. - - - -The content is organized as follows: -1. This summary section -2. Repository information -3. Directory structure -4. Repository files, each consisting of: - - File path as an attribute - - Full contents of the file - - - -- This file should be treated as read-only. Any changes should be made to the - original repository files, not this packed version. -- When processing this file, use the file path to distinguish - between different files in the repository. -- Be aware that this file may contain sensitive information. Handle it with - the same level of security as you would the original repository. - - - -- Some files may have been excluded based on .gitignore rules and Repomix's configuration -- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files -- Files matching patterns in .gitignore are excluded -- Files matching default ignore patterns are excluded -- Files are sorted by Git change count (files with more changes are at the bottom) - - - - - - - - - -categories/ - [cat_id]/ - BasicDetailCard.tsx - page.tsx - TitleCard.tsx - create/ - page.tsx - edit/ - [cat_id]/ - _PROMPT.md - page.tsx - lp-categories-sample-data.tsx - page.tsx -questions/ - [cat_id]/ - BasicDetailCard.tsx - page.tsx - TitleCard.tsx - create/ - page.tsx - edit/ - [cat_id]/ - _PROMPT.md - page.tsx - lp-categories-sample-data.tsx - page.tsx - - - -This section contains the contents of the repository's files. - - -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Card from '@mui/material/Card'; -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 { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; -import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; -import { useTranslation } from 'react-i18next'; - -import { PropertyItem } from '@/components/core/property-item'; -import { PropertyList } from '@/components/core/property-list'; -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; - -export default function BasicDetailCard({ - lpModel: model, - handleEditClick, -}: { - lpModel: MfCategory; - handleEditClick: () => void; -}): React.JSX.Element { - const { t } = useTranslation(); - - return ( - - { - handleEditClick(); - }} - > - - - } - avatar={ - - - - } - title={t('list.basic-details')} - /> - } - orientation="vertical" - sx={{ '--PropertyItem-padding': '12px 24px' }} - > - {( - [ - { - key: 'Customer ID', - value: ( - - ), - }, - { key: 'Name', value: model.cat_name }, - { key: 'Remarks', value: model.remarks }, - { key: 'Description', value: model.description }, - ] satisfies { key: string; value: React.ReactNode }[] - ).map( - (item): React.JSX.Element => ( - - ) - )} - - - ); -} - - - -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; -import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; -import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; -import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; -import { COL_QUIZ_MF_CATEGORIES } from '@/constants'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Grid from '@mui/material/Unstable_Grid2'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import type { RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { pb } from '@/lib/pb'; -import { toast } from '@/components/core/toaster'; -import ErrorDisplay from '@/components/dashboard/error'; -import { defaultMfCategory } from '@/components/dashboard/mf/categories/_constants.ts'; -import { Notifications } from '@/components/dashboard/mf/categories/notifications'; -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; -import FormLoading from '@/components/loading'; - -import BasicDetailCard from './BasicDetailCard'; -import TitleCard from './TitleCard'; - -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({ show: false, detail: '' }); - - // - const [showLessonCategory, setShowLessonCategory] = React.useState(defaultMfCategory); - - function handleEditClick() { - router.push(paths.dashboard.mf_categories.edit(showLessonCategory.id)); - } - - React.useEffect(() => { - if (catId) { - pb.collection(COL_QUIZ_MF_CATEGORIES) - .getOne(catId) - .then((model: RecordModel) => { - setShowLessonCategory({ ...defaultMfCategory, ...model }); - }) - .catch((err) => { - logger.error(err); - toast(t('list.error')); - - setShowError({ show: true, detail: JSON.stringify(err) }); - }) - .finally(() => { - setShowLoading(false); - }); - } - }, [catId]); - - if (showLoading) return ; - if (showError.show) - return ( - - ); - - return ( - - - -
- - - {t('list.title')} - -
- - - -
- - - - - - - - - - - - - - - -
-
- ); -} -
- - -'use client'; - -import * as React from 'react'; -import { Button } from '@mui/material'; -import Avatar from '@mui/material/Avatar'; -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; -import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { useTranslation } from 'react-i18next'; - -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; - -function getImageUrlFrRecord(record: MfCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} - -export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element { - const { t } = useTranslation(); - - return ( - <> - - - {t('empty')} - -
- - {lpModel.cat_name} - - } - label={lpModel.visible} - size="small" - variant="outlined" - /> - - - {lpModel.slug} - -
-
-
- -
- - ); -} -
- - -'use client'; - -// RULES: -// T.B.A. -// -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 { MfCategoryCreateForm } from '@/components/dashboard/mf/categories/mf-category-create-form'; - -export default function Page(): React.JSX.Element { - // RULES: follow the name of page directory - const { t } = useTranslation(['lp_categories']); - - return ( - - - -
- - - {t('title')} - -
-
- {t('create.title')} -
-
- -
-
- ); -} -
- - -# 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, - - - -'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 { MfCategoryEditForm } from '@/components/dashboard/mf/categories/mf-category-edit-form'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(['lp_categories']); - - React.useEffect(() => { - // console.log('helloworld'); - }, []); - - return ( - - - -
- - - {t('edit.title')} - -
-
- {t('edit.title')} -
-
- -
-
- ); -} -
- - -import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; - -export const LpCategoriesSampleData = [ - { - 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[]; - - - -'use client'; - -// RULES: -// contains list page for lp_categories (QuizLPCategories) -// contain definition to collection only -// -import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import { COL_QUIZ_MF_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 { defaultMfCategory } from '@/components/dashboard/mf/categories/_constants'; -import { MfCategoriesFilters } from '@/components/dashboard/mf/categories/mf-categories-filters'; -import type { Filters } from '@/components/dashboard/mf/categories/mf-categories-filters'; -import { MfCategoriesPagination } from '@/components/dashboard/mf/categories/mf-categories-pagination'; -import { MfCategoriesSelectionProvider } from '@/components/dashboard/mf/categories/mf-categories-selection-context'; -import { MfCategoriesTable } from '@/components/dashboard/mf/categories/mf-categories-table'; -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; -import FormLoading from '@/components/loading'; - -export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { t } = useTranslation(['mf_categories']); - 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_QUIZ_MF_CATEGORIES) - .getList(currentPage + 1, rowsPerPage, listOption); - const { items, totalItems } = models; - const tempLessonTypes: MfCategory[] = items.map((lt) => { - return { ...defaultMfCategory, ...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); - } - }; - - 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.mf_categories.create); - }} - startIcon={} - variant="contained" - > - {t('list.add')} - - - - - - - - - - - - - - - - -
{JSON.stringify(f, null, 2)}
-
-
- ); -} - -// Sorting and filtering has to be done on the server. - -function applySort(row: MfCategory[], sortDir: 'asc' | 'desc' | undefined): MfCategory[] { - 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: MfCategory[], { email, phone, status, name, visible }: Filters): MfCategory[] { - 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; - // - }; -} -
- - -'use client'; - -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Card from '@mui/material/Card'; -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 { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; -import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; -import { useTranslation } from 'react-i18next'; - -import { PropertyItem } from '@/components/core/property-item'; -import { PropertyList } from '@/components/core/property-list'; -import { MfCategory } from '@/components/dashboard/mf/categories/type'; - -export default function BasicDetailCard({ - lpModel: model, - handleEditClick, -}: { - lpModel: MfCategory; - handleEditClick: () => void; -}): React.JSX.Element { - const { t } = useTranslation(); - - return ( - - { - handleEditClick(); - }} - > - - - } - avatar={ - - - - } - title={t('list.basic-details')} - /> - } - orientation="vertical" - sx={{ '--PropertyItem-padding': '12px 24px' }} - > - {( - [ - { - key: 'Customer ID', - value: ( - - ), - }, - { key: 'Name', value: model.cat_name }, - { key: 'Remarks', value: model.remarks }, - { key: 'Description', value: model.description }, - ] satisfies { key: string; value: React.ReactNode }[] - ).map( - (item): React.JSX.Element => ( - - ) - )} - - - ); -} - - - -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -import SampleAddressCard from '@/app/dashboard/Sample/AddressCard'; -import { SampleNotifications } from '@/app/dashboard/Sample/Notifications'; -import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; -import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; -import { COL_QUIZ_MF_QUESTIONS } from '@/constants'; -import { Grid } from '@mui/material'; -import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; -import type { RecordModel } from 'pocketbase'; -import { useTranslation } from 'react-i18next'; - -import { paths } from '@/paths'; -import { logger } from '@/lib/default-logger'; -import { pb } from '@/lib/pb'; -import { toast } from '@/components/core/toaster'; -import ErrorDisplay from '@/components/dashboard/error'; -import { defaultMfQuestion } from '@/components/dashboard/mf/questions/_constants.ts'; -import { Notifications } from '@/components/dashboard/mf/questions/notifications'; -import type { MfQuestion } from '@/components/dashboard/mf/questions/type'; -import FormLoading from '@/components/loading'; - -import BasicDetailCard from './BasicDetailCard'; -import TitleCard from './TitleCard'; - -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({ show: false, detail: '' }); - - // - const [showLessonQuestion, setShowLessonQuestion] = React.useState(defaultMfQuestion); - - function handleEditClick() { - router.push(paths.dashboard.mf_questions.edit(showLessonQuestion.id)); - } - - React.useEffect(() => { - if (catId) { - pb.collection(COL_QUIZ_MF_QUESTIONS) - .getOne(catId) - .then((model: RecordModel) => { - setShowLessonQuestion({ ...defaultMfQuestion, ...model }); - }) - .catch((err) => { - logger.error(err); - toast(t('list.error')); - - setShowError({ show: true, detail: JSON.stringify(err) }); - }) - .finally(() => { - setShowLoading(false); - }); - } - }, [catId]); - - if (showLoading) return ; - if (showError.show) - return ( - - ); - - return ( - - - -
- - - {t('edit.title')} - -
- - - -
- - - - - - - - - - - - - - - -
-
- ); -} -
- - -'use client'; - -import * as React from 'react'; -import { Button } from '@mui/material'; -import Avatar from '@mui/material/Avatar'; -import Chip from '@mui/material/Chip'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; -import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; -import { useTranslation } from 'react-i18next'; - -import type { MfCategory } from '@/components/dashboard/mf/categories/type'; - -function getImageUrlFrRecord(record: MfCategory): string { - return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`; -} - -export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element { - const { t } = useTranslation(); - - return ( - <> - - - {t('empty')} - -
- - {lpModel.cat_name} - - } - label={lpModel.visible} - size="small" - variant="outlined" - /> - - - {lpModel.slug} - -
-
-
- -
- - ); -} -
- - -'use client'; - -// RULES: -// T.B.A. -// -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 { MfQuestionCreateForm } from '@/components/dashboard/mf/questions/mf-question-create-form'; - -export default function Page(): React.JSX.Element { - // RULES: follow the name of page directory - const { t } = useTranslation(['lp_questions']); - - return ( - - - -
- - - {t('title')} - -
-
- {t('create.title')} -
-
- -
-
- ); -} -
- - -# 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, - - - -'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 { MfQuestionEditForm } from '@/components/dashboard/mf/questions/mf-question-edit-form'; - -export default function Page(): React.JSX.Element { - const { t } = useTranslation(['lp_questions']); - - React.useEffect(() => { - // console.log('helloworld'); - }, []); - - return ( - - - -
- - - {t('edit.title')} - -
-
- {t('edit.title')} -
-
- -
-
- ); -} -
- - -import { dayjs } from '@/lib/dayjs'; -import { LessonCategory } from '@/components/dashboard/lesson_category/type'; - -export const LpCategoriesSampleData = [ - { - 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[]; - - - -'use client'; - -// RULES: -// contains list page for lp_questions (QuizLPQuestions) -// contain definition to collection only -// -import * as React from 'react'; -import { useRouter } from 'next/navigation'; -import { COL_QUIZ_MF_QUESTIONS } 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 { defaultMfQuestion } from '@/components/dashboard/mf/questions/_constants'; -import { MfQuestionsFilters } from '@/components/dashboard/mf/questions/mf-questions-filters'; -import type { Filters } from '@/components/dashboard/mf/questions/mf-questions-filters'; -import { MfQuestionsPagination } from '@/components/dashboard/mf/questions/mf-questions-pagination'; -import { MfQuestionsSelectionProvider } from '@/components/dashboard/mf/questions/mf-questions-selection-context'; -import { MfQuestionsTable } from '@/components/dashboard/mf/questions/mf-questions-table'; -import type { MfQuestion } from '@/components/dashboard/mf/questions/type'; -import FormLoading from '@/components/loading'; - -export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { t } = useTranslation(['mf_question']); - const { email, phone, sortDir, status, name, visible, type } = searchParams; - const router = useRouter(); - const [lessonQuestionsData, 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(0); - const [recordCount, setRecordCount] = React.useState(0); - const [listOption, setListOption] = React.useState({}); - const [listSort, setListSort] = React.useState({}); - - // - const sortedLessonCategories = applySort(lessonQuestionsData, sortDir); - const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status }); - - const reloadRows = async (): Promise => { - try { - const models: ListResult = await pb - .collection(COL_QUIZ_MF_QUESTIONS) - .getList(currentPage + 1, rowsPerPage, listOption); - const { items, totalItems } = models; - const tempLessonTypes: MfQuestion[] = items.map((lt) => { - return { ...defaultMfQuestion, ...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); - } - }; - - 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.lp_questions.create); - }} - startIcon={} - variant="contained" - > - {t('list.add')} - - - - - - - - - - - - - - - - -
{JSON.stringify(f, null, 2)}
-
-
- ); -} - -// Sorting and filtering has to be done on the server. - -function applySort(row: MfQuestion[], sortDir: 'asc' | 'desc' | undefined): MfQuestion[] { - 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: MfQuestion[], { email, phone, status, name, visible }: Filters): MfQuestion[] { - 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/page.tsx b/002_source/cms/src/app/dashboard/page.tsx index d5c837d..ce4374c 100644 --- a/002_source/cms/src/app/dashboard/page.tsx +++ b/002_source/cms/src/app/dashboard/page.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import GetAllLessonCategoriesCount from '@/db/LessonCategories/GetAllCount'; import GetAllLessonTypesCount from '@/db/LessonTypes/GetAllCount'; -import GetAllUsersCount from '@/db/Users/GetAllCount'; +import { GetAllUsersCount } from '@/db/Users/GetAllCount'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Stack from '@mui/material/Stack'; @@ -59,21 +59,37 @@ export default function Page(): React.JSX.Element { }} > - + {t('Overview')}
-
- - + + - + - + - + - + - + - + - + - + } size="small"> + } @@ -134,10 +175,17 @@ export default function Page(): React.JSX.Element { title={t('Find your dream job')} /> - + } size="small"> + } @@ -147,10 +195,17 @@ export default function Page(): React.JSX.Element { title={t('Need help figuring things out?')} /> - + } size="small"> + } diff --git a/002_source/cms/src/app/dashboard/students/SampleStudents.tsx b/002_source/cms/src/app/dashboard/students/SampleStudents.tsx new file mode 100644 index 0000000..f39ad4d --- /dev/null +++ b/002_source/cms/src/app/dashboard/students/SampleStudents.tsx @@ -0,0 +1,57 @@ +// src/app/dashboard/students/page.tsx +'use client'; +import type { Student } from '@/db/Students/type.d'; +import { dayjs } from '@/lib/dayjs'; + +export const SampleStudents = [ + { + id: 'STU-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'STU-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'STU-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'STU-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'STU-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, +] satisfies Student[]; diff --git a/002_source/cms/src/app/dashboard/students/_GUIDELINES.md b/002_source/cms/src/app/dashboard/students/_GUIDELINES.md index 1353901..999a5e2 100644 --- a/002_source/cms/src/app/dashboard/students/_GUIDELINES.md +++ b/002_source/cms/src/app/dashboard/students/_GUIDELINES.md @@ -1,11 +1,11 @@ # GUIDELINES -this folder is part of nextjs typescript project and containing page definition for `Customer` / `Customers` record: +this folder is part of nextjs typescript project and containing page definition for `Student` / `Students` record: - list (./page.tsx) -- view (./[customerId]/page.tsx) +- view (./[studentId]/page.tsx) - create (./create/page.tsx) -- edit (./[customerId]/page.tsx) +- edit (./[studentId]/page.tsx) - translation provided by react-i18next the `@` sign refer to `/002_source/002_source/cms/src` @@ -13,17 +13,17 @@ the `@` sign refer to `/002_source/002_source/cms/src` ## Assumption and Requirements - let one file contains one component only. -- type information defined in `/002_source/cms/src/db/Customers/type.d.tsx` -- it mainly consume the db drivers `Customres` in `/002_source/cms/src/db/Customers` +- type information defined in `/002_source/cms/src/db/Students/type.d.tsx` +- it mainly consume the db drivers `Students` in `/002_source/cms/src/db/Students` simple template: ```typescript -// src/app/dashboard/customers/page.tsx +// src/app/dashboard/students/page.tsx 'use client'; // RULES: -// contains list page for customers (Customers) +// contains list page for students (Students) // contain definition to collection only // import statements here ... @@ -46,4 +46,3 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { interface PageProps { searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string }; } -``` diff --git a/002_source/cms/src/app/dashboard/students/edit/[customerId]/_PROMPT.md b/002_source/cms/src/app/dashboard/students/edit/[customerId]/_PROMPT.md deleted file mode 100644 index abf4465..0000000 --- a/002_source/cms/src/app/dashboard/students/edit/[customerId]/_PROMPT.md +++ /dev/null @@ -1,11 +0,0 @@ -# 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/students/list/page.tsx b/002_source/cms/src/app/dashboard/students/list/page.tsx index 5dcb6d0..fd68afb 100644 --- a/002_source/cms/src/app/dashboard/students/list/page.tsx +++ b/002_source/cms/src/app/dashboard/students/list/page.tsx @@ -1,13 +1,13 @@ -// src/app/dashboard/customers/page.tsx +// src/app/dashboard/students/list/page.tsx 'use client'; // RULES: -// contains list page for customers (Customers) +// contains list page for students (Students) // contain definition to collection only // import * as React from 'react'; import { useRouter } from 'next/navigation'; -import { COL_CUSTOMERS } from '@/constants'; +import { COL_STUDENTS } from '@/constants'; import { LoadingButton } from '@mui/lab'; import Box from '@mui/material/Box'; import Card from '@mui/material/Card'; @@ -33,7 +33,7 @@ import { defaultStudent } from '@/components/dashboard/student/_constants'; import FormLoading from '@/components/loading'; export default function Page({ searchParams }: PageProps): React.JSX.Element { - const { t } = useTranslation(['customers']); + const { t } = useTranslation(['students']); const router = useRouter(); const { email, phone, sortDir, status } = searchParams; @@ -57,7 +57,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element { const reloadRows = async (): Promise => { try { const models: ListResult = await pb - .collection(COL_CUSTOMERS) + .collection(COL_STUDENTS) .getList(currentPage + 1, rowsPerPage, listOption); const { items, totalItems } = models; const tempLessonTypes: Student[] = items.map((lt) => { diff --git a/002_source/cms/src/components/dashboard/layout/config.ts b/002_source/cms/src/components/dashboard/layout/config.ts index d62368e..e3ffd62 100644 --- a/002_source/cms/src/components/dashboard/layout/config.ts +++ b/002_source/cms/src/components/dashboard/layout/config.ts @@ -111,7 +111,16 @@ export const layoutConfig = { }, ], }, - + { + key: 'users', + title: 'users', + icon: 'users', + items: [ + { key: 'users', title: 'List users', href: paths.dashboard.users.list }, + { key: 'users:create', title: 'Create user', href: paths.dashboard.users.create }, + { key: 'users:details', title: 'User details', href: paths.dashboard.users.view('1') }, + ], + }, { key: 'teachers', title: 'teachers', diff --git a/002_source/cms/src/components/dashboard/lp/questions/type.d.ts b/002_source/cms/src/components/dashboard/lp/questions/type.d.ts index 8975fb4..7e2f209 100644 --- a/002_source/cms/src/components/dashboard/lp/questions/type.d.ts +++ b/002_source/cms/src/components/dashboard/lp/questions/type.d.ts @@ -1,3 +1,4 @@ +// QuizLPQuestion export interface LpQuestion { isEmpty?: boolean; // diff --git a/002_source/cms/src/components/dashboard/student/_GUIDELINES.md b/002_source/cms/src/components/dashboard/student/_GUIDELINES.md index 709449f..4936017 100644 --- a/002_source/cms/src/components/dashboard/student/_GUIDELINES.md +++ b/002_source/cms/src/components/dashboard/student/_GUIDELINES.md @@ -1,25 +1,44 @@ -# GUIDELINES & KEY COMPONENTS +# STUDENT MANAGEMENT GUIDELINES -- `_constants.ts` contains the constant for +## Core Components - - default value (defaultValue) - - empty value (emptyValue) +- `_constants.ts` - Contains essential constants for: + - Default values (defaultValue) + - Empty/placeholder values (emptyValue) + - Student status options (active/inactive) + - Grade level options -- `customers-table.tsx` +## Student Data Table System -- `confirm-delete-modal.tsx` - delete modal component when click delete button on list +- `students-table.tsx` - Main student listing table +- `confirm-delete-modal.tsx` - Confirmation dialog for student deletion +- `students-selection-context.tsx` - Context for selected student records - - `customers-filters.tsx` - - `customers-pagination.tsx` - - `email-filter-popover.tsx` - - `phone-filter-popover.tsx` - - `customers-selection-context.tsx` +### Table Features -- `customer-create-form.tsx` - form to create a new customer -- `customer-edit-form.tsx` - form to edit a existing customer +- `students-filters.tsx` - Filter controls for the student table + - `email-filter-popover.tsx` - Email-specific filtering + - `phone-filter-popover.tsx` - Phone number filtering +- `students-pagination.tsx` - Pagination controls -- `type.d.tsx` - contains type definition +## Student Forms -- `notifications.tsx` - constants used for demonstration -- `payments.tsx` - constants used for demonstration -- `shipping-address.tsx` - constants used for demonstration +- `student-create-form.tsx` - Form for adding new students with: + - Personal information fields + - Contact details + - Enrollment information +- `student-edit-form.tsx` - Form for modifying existing student records + +## Supporting Components + +- `type.d.tsx` - Type definitions for student data +- `notifications.tsx` - Student notification preferences +- `payments.tsx` - Student payment records +- `shipping-address.tsx` - Student address information + +## Best Practices + +1. Always validate student data before submission +2. Use the provided constants for default/empty values +3. Maintain consistent field naming across all components +4. Follow accessibility guidelines for all form inputs diff --git a/002_source/cms/src/components/dashboard/user/_GUIDELINES.md b/002_source/cms/src/components/dashboard/user/_GUIDELINES.md new file mode 100644 index 0000000..9e5a150 --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/_GUIDELINES.md @@ -0,0 +1,25 @@ +# GUIDELINES & KEY COMPONENTS + +- `_constants.ts` contains the constant for + + - default value (defaultValue) + - empty value (emptyValue) + +- `users-table.tsx` + +- `confirm-delete-modal.tsx` - delete modal component when click delete button on list + + - `users-filters.tsx` + - `users-pagination.tsx` + - `email-filter-popover.tsx` + - `phone-filter-popover.tsx` + - `users-selection-context.tsx` + +- `user-create-form.tsx` - form to create a new user +- `user-edit-form.tsx` - form to edit a existing user + +- `type.d.tsx` - contains type definition + +- `notifications.tsx` - constants used for demonstration +- `payments.tsx` - constants used for demonstration +- `shipping-address.tsx` - constants used for demonstration diff --git a/002_source/cms/src/components/dashboard/user/_constants.ts b/002_source/cms/src/components/dashboard/user/_constants.ts new file mode 100644 index 0000000..62097e2 --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/_constants.ts @@ -0,0 +1,21 @@ +// RULES: +// default variable value for customer +// empty valur for customer + +import { dayjs } from '@/lib/dayjs'; +import type { User } from './type.d'; + +export const defaultUser: User = { + id: '', + name: '', + avatar: undefined, + email: '', + phone: undefined, + quota: 0, + status: 'pending', + createdAt: dayjs().toDate(), +}; + +export const emptyLpCategory: User = { + ...defaultUser, +}; diff --git a/002_source/cms/src/components/dashboard/user/confirm-delete-modal.tsx b/002_source/cms/src/components/dashboard/user/confirm-delete-modal.tsx new file mode 100644 index 0000000..3a4f946 --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/confirm-delete-modal.tsx @@ -0,0 +1,124 @@ +'use client'; + +import * as React from 'react'; +import { LoadingButton } from '@mui/lab'; +import { Button, Container, Modal, Paper } from '@mui/material'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note'; +import { useTranslation } from 'react-i18next'; + +import { logger } from '@/lib/default-logger'; +import { toast } from '@/components/core/toaster'; +import { deleteUser } from '@/db/Users/Delete'; + +export default function ConfirmDeleteModal({ + open, + setOpen, + idToDelete, + reloadRows, +}: { + open: boolean; + setOpen: (b: boolean) => void; + idToDelete: string; + reloadRows: () => void; +}): React.JSX.Element { + const { t } = useTranslation(); + + // const handleClose = () => setOpen(false); + function handleClose(): void { + setOpen(false); + } + + const [isDeleteing, setIsDeleteing] = React.useState(false); + const style = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + }; + + function handleUserConfirmDelete(): void { + if (idToDelete) { + setIsDeleteing(true); + + // RULES: delete + deleteUser(idToDelete) + .then(() => { + reloadRows(); + handleClose(); + toast(t('delete.success')); + }) + .catch((err) => { + // console.error(err) + logger.error(err); + toast(t('delete.error')); + }) + .finally(() => { + setIsDeleteing(false); + }); + } + } + + return ( +
+ + + + + + + + + + + {t('Delete User ?')} + + {t('Are you sure you want to delete this user ?')} + + + + + { + handleUserConfirmDelete(); + }} + loading={isDeleteing} + > + {t('Delete')} + + + + + + + + +
+ ); +} diff --git a/002_source/cms/src/components/dashboard/user/email-filter-popover.tsx b/002_source/cms/src/components/dashboard/user/email-filter-popover.tsx new file mode 100644 index 0000000..2636af0 --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/email-filter-popover.tsx @@ -0,0 +1,50 @@ +'use client'; + +import * as React from 'react'; + +import Button from '@mui/material/Button'; +import FormControl from '@mui/material/FormControl'; +import OutlinedInput from '@mui/material/OutlinedInput'; + +import { FilterPopover, useFilterContext } from '@/components/core/filter-button'; + +// EmailFilterPopover -> email-filter-popover.tsx +export default 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} + /> + + + + ); +} diff --git a/002_source/cms/src/components/dashboard/user/helloworld.tsx b/002_source/cms/src/components/dashboard/user/helloworld.tsx new file mode 100644 index 0000000..3989cb1 --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/helloworld.tsx @@ -0,0 +1,3 @@ +const helloworld = 'helloworld'; + +export { helloworld }; diff --git a/002_source/cms/src/components/dashboard/user/notifications.tsx b/002_source/cms/src/components/dashboard/user/notifications.tsx new file mode 100644 index 0000000..a6c16bd --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/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/user/payments.tsx b/002_source/cms/src/components/dashboard/user/payments.tsx new file mode 100644 index 0000000..0420d32 --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/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/user/phone-filter-popover.tsx b/002_source/cms/src/components/dashboard/user/phone-filter-popover.tsx new file mode 100644 index 0000000..08cbad7 --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/phone-filter-popover.tsx @@ -0,0 +1,50 @@ +'use client'; + +import * as React from 'react'; + +import Button from '@mui/material/Button'; +import FormControl from '@mui/material/FormControl'; +import OutlinedInput from '@mui/material/OutlinedInput'; + +import { FilterPopover, useFilterContext } from '@/components/core/filter-button'; + +// phone-filter-popover.tsx +export default 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/user/shipping-address.tsx b/002_source/cms/src/components/dashboard/user/shipping-address.tsx new file mode 100644 index 0000000..8793e5c --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/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/user/type.d.tsx b/002_source/cms/src/components/dashboard/user/type.d.tsx new file mode 100644 index 0000000..847764a --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/type.d.tsx @@ -0,0 +1,69 @@ +'use client'; + +export type SortDir = 'asc' | 'desc'; + +export interface User { + id: string; + name: string; + avatar?: string; + email: string; + phone?: string; + quota: number; + status: 'pending' | 'active' | 'blocked'; + createdAt: Date; + updatedAt?: Date; +} + +export interface CreateFormProps { + name: string; + email: string; + phone?: string; + company?: string; + billingAddress?: { + country: string; + state: string; + city: string; + zipCode: string; + line1: string; + line2?: string; + }; + taxId?: string; + timezone: string; + language: string; + currency: string; + avatar?: string; + // quota?: number; + // status?: 'pending' | 'active' | 'blocked'; +} + +export interface EditFormProps { + name: string; + email: string; + phone?: string; + company?: string; + billingAddress?: { + country: string; + state: string; + city: string; + zipCode: string; + line1: string; + line2?: string; + }; + taxId?: string; + timezone: string; + language: string; + currency: string; + avatar?: string; + // quota?: number; + // status?: 'pending' | 'active' | 'blocked'; +} +export interface CustomersFiltersProps { + filters?: Filters; + sortDir?: SortDir; + fullData: User[]; +} +export interface Filters { + email?: string; + phone?: string; + status?: string; +} diff --git a/002_source/cms/src/components/dashboard/user/user-create-form.tsx b/002_source/cms/src/components/dashboard/user/user-create-form.tsx new file mode 100644 index 0000000..da9316b --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/user-create-form.tsx @@ -0,0 +1,529 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { zodResolver } from '@hookform/resolvers/zod'; +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 Divider from '@mui/material/Divider'; +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 { Controller, useForm } from 'react-hook-form'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { logger } from '@/lib/default-logger'; +import { Option } from '@/components/core/option'; +import { toast } from '@/components/core/toaster'; +import { createCustomer as createUser } from '@/db/Customers/Create'; +import isDevelopment from '@/lib/check-is-development'; + +function fileToBase64(file: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + resolve(reader.result as string); + }; + reader.onerror = () => { + reject(new Error('Error converting file to base64')); + }; + }); +} + +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: 'new name', + email: '123@123.com', + phone: '91234567', + company: '', + billingAddress: { + country: 'US', + state: '00000', + city: 'NY', + zipCode: '00000', + line1: 'test line 1', + line2: 'test line 2', + }, + taxId: '12345', + timezone: 'new_york', + language: 'en', + currency: 'USD', +} satisfies Values; + +export function UserCreateForm(): React.JSX.Element { + const router = useRouter(); + + const { + control, + handleSubmit, + formState: { errors }, + setValue, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + try { + // Use standard create method from db/Customers/Create + const record = await createUser(values); + toast.success('User created successfully'); + router.push(paths.dashboard.users.details(record.id)); + } catch (err) { + logger.error(err); + toast.error('Failed to create user. Please try again.'); + } + }, + [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} + > + + Account information + + + + + + + + + + Avatar + Min 400x400px, PNG or JPEG + + + + + + + ( + + Name + + {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} + + )} + /> + + + + + Billing information + + + ( + + Country + + {errors.billingAddress?.country ? ( + {errors.billingAddress?.country?.message} + ) : null} + + )} + /> + + + ( + + State + + {errors.billingAddress?.state ? ( + {errors.billingAddress?.state?.message} + ) : null} + + )} + /> + + + ( + + City + + {errors.billingAddress?.city ? ( + {errors.billingAddress?.city?.message} + ) : null} + + )} + /> + + + ( + + Zip code + + {errors.billingAddress?.zipCode ? ( + {errors.billingAddress?.zipCode?.message} + ) : null} + + )} + /> + + + ( + + Address + + {errors.billingAddress?.line1 ? ( + {errors.billingAddress?.line1?.message} + ) : null} + + )} + /> + + + ( + + Tax ID + + {errors.taxId ? {errors.taxId.message} : null} + + )} + /> + + + + + Shipping information + } + label="Same as billing address" + /> + + + Additional information + + + ( + + Timezone + + {errors.timezone ? {errors.timezone.message} : null} + + )} + /> + + + ( + + Language + + {errors.language ? {errors.language.message} : null} + + )} + /> + + + ( + + Currency + + {errors.currency ? {errors.currency.message} : null} + + )} + /> + + + + + + + + + + + +
{JSON.stringify({ errors }, null, 2)}
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/user/user-edit-form.tsx b/002_source/cms/src/components/dashboard/user/user-edit-form.tsx new file mode 100644 index 0000000..8effa39 --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/user-edit-form.tsx @@ -0,0 +1,604 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +// +import { COL_USERS } from '@/constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; +// +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +// +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +// +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { logger } from '@/lib/default-logger'; +import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64'; +import { pb } from '@/lib/pb'; +import { toast } from '@/components/core/toaster'; +import FormLoading from '@/components/loading'; + +// import ErrorDisplay from '../../error'; +import ErrorDisplay from '../error'; +import isDevelopment from '@/lib/check-is-development'; + +// TODO: review this +const schema = zod.object({ + 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(25), + company: zod.string().max(255).optional(), + 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), + avatar: zod.string().optional(), +}); + +type Values = zod.infer; + +const defaultValues = { + name: '', + email: '', + phone: '', + company: '', + billingAddress: { + country: '', + state: '', + city: '', + zipCode: '', + line1: '', + line2: '', + }, + taxId: '', + timezone: '', + language: '', + currency: '', + avatar: '', +} satisfies Values; + +export function UserEditForm(): React.JSX.Element { + const router = useRouter(); + const { t } = useTranslation(['lp_categories']); + + const { id: userId } = useParams<{ userId: string }>(); + // + const [isUpdating, setIsUpdating] = React.useState(false); + const [showLoading, setShowLoading] = React.useState(false); + // + const [showError, setShowError] = React.useState({ show: false, detail: '' }); + + const { + control, + handleSubmit, + formState: { errors }, + setValue, + reset, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (values: Values): Promise => { + setIsUpdating(true); + + const updateData = { + name: values.name, + email: values.email, + phone: values.phone, + company: values.company, + billingAddress: values.billingAddress, + taxId: values.taxId, + timezone: values.timezone, + language: values.language, + currency: values.currency, + avatar: values.avatar ? await base64ToFile(values.avatar) : null, + }; + + try { + await pb.collection(COL_USERS).update(userId, updateData); + toast.success('User updated successfully'); + router.push(paths.dashboard.users.list); + } catch (error) { + logger.error(error); + toast.error('Failed to update user'); + } finally { + setIsUpdating(false); + } + }, + [userId, router] + ); + + const avatarInputRef = React.useRef(null); + const avatar = watch('avatar'); + + const handleAvatarChange = React.useCallback( + async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + + if (file) { + const url = await fileToBase64(file); + setValue('avatar', url); + } + }, + [setValue] + ); + + // TODO: need to align with save form + // use trycatch + const [textDescription, setTextDescription] = React.useState(''); + const [textRemarks, setTextRemarks] = React.useState(''); + + // load existing data when user arrive + const loadExistingData = React.useCallback( + async (id: string) => { + setShowLoading(true); + + try { + const result = await pb.collection(COL_USERS).getOne(id); + reset({ ...defaultValues, ...result }); + console.log({ result }); + + if (result.avatar_file) { + const fetchResult = await fetch( + `http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.avatar_file}` + ); + const blob = await fetchResult.blob(); + const url = await fileToBase64(blob); + setValue('avatar', url); + } + } catch (error) { + logger.error(error); + toast.error('Failed to load user data'); + setShowError({ show: true, detail: JSON.stringify(error, null, 2) }); + } finally { + setShowLoading(false); + } + }, + [reset, setValue] + ); + + React.useEffect(() => { + void loadExistingData(userId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userId]); + + if (showLoading) return ; + if (showError.show) + return ( + + ); + + return ( +
+ + + } + spacing={4} + > + + {t('edit.basic-info')} + + + + + + + + + + {t('edit.avatar')} + {t('edit.avatarRequirements')} + + + + + + + ( + + Name + + {errors.name ? {errors.name.message} : null} + + )} + /> + + + ( + + Email + + {errors.email ? {errors.email.message} : null} + + )} + /> + + + ( + + Phone + + {errors.phone ? {errors.phone.message} : null} + + )} + /> + + + ( + + Company + + {errors.company ? {errors.company.message} : null} + + )} + /> + + + + {/* */} + + Billing Information + + + ( + + Country + + {errors.billingAddress?.country ? ( + {errors.billingAddress.country.message} + ) : null} + + )} + /> + + + ( + + State + + {errors.billingAddress?.state ? ( + {errors.billingAddress.state.message} + ) : null} + + )} + /> + + + ( + + City + + {errors.billingAddress?.city ? ( + {errors.billingAddress.city.message} + ) : null} + + )} + /> + + + ( + + Zip Code + + {errors.billingAddress?.zipCode ? ( + {errors.billingAddress.zipCode.message} + ) : null} + + )} + /> + + + ( + + Address Line 1 + + {errors.billingAddress?.line1 ? ( + {errors.billingAddress.line1.message} + ) : null} + + )} + /> + + + ( + + Tax ID + + {errors.taxId ? {errors.taxId.message} : null} + + )} + /> + + + + + + Additional Information + + + ( + + Timezone + + {errors.timezone ? {errors.timezone.message} : null} + + )} + /> + + + ( + + Language + + {errors.language ? {errors.language.message} : null} + + )} + /> + + + ( + + Currency + + {errors.currency ? {errors.currency.message} : null} + + )} + /> + + + + + + + + + + {t('edit.updateButton')} + + + + +
{JSON.stringify({ errors }, null, 2)}
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/user/users-filters.tsx b/002_source/cms/src/components/dashboard/user/users-filters.tsx new file mode 100644 index 0000000..418c7eb --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/users-filters.tsx @@ -0,0 +1,242 @@ +'use client'; +// RULES: +// T.B.A. +// +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { getAllCustomersCount } from '@/db/Customers/GetAllCount'; + +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import Select from '@mui/material/Select'; +import type { SelectChangeEvent } from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import { paths } from '@/paths'; +import { FilterButton } from '@/components/core/filter-button'; +import { Option } from '@/components/core/option'; + +import { useCustomersSelection } from './users-selection-context'; +import GetBlockedCount from '@/db/Customers/GetBlockedCount'; +import GetPendingCount from '@/db/Customers/GetPendingCount'; +import GetActiveCount from '@/db/Customers/GetActiveCount'; +import PhoneFilterPopover from './phone-filter-popover'; +import EmailFilterPopover from './email-filter-popover'; +import type { CustomersFiltersProps, Filters, SortDir } from './type.d'; + +export function UsersFilters({ filters = {}, sortDir = 'desc', fullData }: CustomersFiltersProps): React.JSX.Element { + const { t } = useTranslation(); + + const { email, phone, status } = filters; + + const [totalCount, setTotalCount] = React.useState(0); + const [activeCount, setActiveCount] = React.useState(0); + const [pendingCount, setPendingCount] = React.useState(0); + const [blockedCount, setBlockedCount] = React.useState(0); + + const router = useRouter(); + + const selection = useCustomersSelection(); + + // function getVisible(): number { + // return fullData.reduce((count, item: CrQuestion) => { + // return item.visible === 'visible' ? count + 1 : count; + // }, 0); + // } + + // function getHidden(): number { + // return fullData.reduce((count, item: CrQuestion) => { + // return item.visible === 'hidden' ? count + 1 : count; + // }, 0); + // } + + // The tabs should be generated using API data. + const tabs = [ + { label: 'All', value: '', count: totalCount }, + { label: 'Active', value: 'active', count: activeCount }, + { label: 'Pending', value: 'pending', count: pendingCount }, + { label: 'Blocked', value: 'blocked', count: blockedCount }, + ] 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); + } + + router.push(`${paths.dashboard.customers.list}?${searchParams.toString()}`); + }, + [router] + ); + + const handleClearFilters = React.useCallback(() => { + updateSearchParams({}, sortDir); + }, [updateSearchParams, sortDir]); + + const handleStatusChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, status: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleEmailChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, email: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handlePhoneChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, phone: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleSortChange = React.useCallback( + (event: SelectChangeEvent) => { + updateSearchParams(filters, event.target.value as SortDir); + }, + [updateSearchParams, filters] + ); + + const hasFilters = status || email || phone; + + React.useEffect(() => { + const fetchCount = async (): Promise => { + try { + const tc = await getAllCustomersCount(); + setTotalCount(tc); + + const bc = await GetBlockedCount(); + setBlockedCount(bc); + const pc = await GetPendingCount(); + setPendingCount(pc); + const ac = await GetActiveCount(); + setActiveCount(ac); + } catch (error) { + // + } + }; + void fetchCount(); + }, []); + + return ( +
+ + {tabs.map((tab) => ( + + } + iconPosition="end" + key={tab.value} + label={tab.label} + sx={{ minHeight: 'auto' }} + tabIndex={0} + value={tab.value} + /> + ))} + + + + + { + handleEmailChange(value as string); + }} + onFilterDelete={() => { + handleEmailChange(); + }} + popover={} + value={email} + /> + + { + handlePhoneChange(value as string); + }} + onFilterDelete={() => { + handlePhoneChange(); + }} + popover={} + value={phone} + /> + + {hasFilters ? : null} + + {selection.selectedAny ? ( + + + {selection.selected.size} selected + + + + ) : null} + + +
+ ); +} diff --git a/002_source/cms/src/components/dashboard/user/users-pagination.tsx b/002_source/cms/src/components/dashboard/user/users-pagination.tsx new file mode 100644 index 0000000..1d92ade --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/users-pagination.tsx @@ -0,0 +1,50 @@ +'use client'; + +import * as React from 'react'; +import TablePagination from '@mui/material/TablePagination'; + +function noop(): void { + return undefined; +} + +interface UsersPaginationProps { + count: number; + page: number; + // + setPage: (page: number) => void; + setRowsPerPage: (page: number) => void; + rowsPerPage: number; +} + +export function UsersPagination({ + count, + page, + // + setPage, + setRowsPerPage, + rowsPerPage, +}: UsersPaginationProps): 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/user/users-selection-context.tsx b/002_source/cms/src/components/dashboard/user/users-selection-context.tsx new file mode 100644 index 0000000..f2a5c1e --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/users-selection-context.tsx @@ -0,0 +1,40 @@ +'use client'; + +import * as React from 'react'; + +import { useSelection } from '@/hooks/use-selection'; +import type { Selection } from '@/hooks/use-selection'; + +import type { User } from './type.d'; + +function noop(): void { + return undefined; +} + +export interface CustomersSelectionContextValue extends Selection {} + +export const CustomersSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface UsersSelectionProviderProps { + children: React.ReactNode; + users: User[]; +} + +export function UsersSelectionProvider({ children, users = [] }: UsersSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => users.map((customer) => customer.id), [users]); + const selection = useSelection(customerIds); + + return {children}; +} + +export function useCustomersSelection(): CustomersSelectionContextValue { + return React.useContext(CustomersSelectionContext); +} diff --git a/002_source/cms/src/components/dashboard/user/users-table.tsx b/002_source/cms/src/components/dashboard/user/users-table.tsx new file mode 100644 index 0000000..a607330 --- /dev/null +++ b/002_source/cms/src/components/dashboard/user/users-table.tsx @@ -0,0 +1,222 @@ +'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 { useCustomersSelection } from './users-selection-context'; +import type { User } from './type.d'; + +function columns(handleDeleteClick: (testId: string) => void): ColumnDef[] { + return [ + { + formatter: (row): React.JSX.Element => ( + + {' '} +
+ + {row.name} + + + {row.email} + +
+
+ ), + name: 'Name', + width: '250px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} + + + ), + name: 'Quota', + width: '150px', + }, + { field: 'phone', name: 'Phone number', width: '150px' }, + + { + 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: ( + + ), + }, + } 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: '150px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + + { + handleDeleteClick(row.id); + }} + > + + + + ), + name: 'Actions', + hideName: true, + align: 'right', + }, + ]; +} + +export interface UsersTableProps { + rows: User[]; + reloadRows: () => void; +} + +export function UsersTable({ rows, reloadRows }: UsersTableProps): React.JSX.Element { + const { t } = useTranslation(['customers']); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCustomersSelection(); + + 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: update this */} + {t('no-record-found')} + + + ) : null} + + ); +} diff --git a/002_source/cms/src/constants.ts b/002_source/cms/src/constants.ts index d4ffcbd..f20a8e7 100644 --- a/002_source/cms/src/constants.ts +++ b/002_source/cms/src/constants.ts @@ -1,6 +1,6 @@ // RULES: // COL_ = "" -// e.g. COL_APPLE = "Apple" table in dbml +// e.g. COL_APPLE = collection "Apple" in pocketbase = "Apple" table in dbml const COL_LESSON_TYPES = 'LessonsTypes'; const COL_LESSON_CATEGORIES = 'LessonsCategories'; const COL_USERS = 'users'; diff --git a/002_source/cms/src/db/Customers/GetActiveCount.tsx b/002_source/cms/src/db/Customers/GetActiveCount.tsx index a8679de..4a2f04d 100644 --- a/002_source/cms/src/db/Customers/GetActiveCount.tsx +++ b/002_source/cms/src/db/Customers/GetActiveCount.tsx @@ -1,9 +1,9 @@ -import { COL_CUSTOMERS } from '@/constants'; +import { COL_CUSTOMERS, COL_USER_METAS } from '@/constants'; import { pb } from '@/lib/pb'; export default async function GetActiveCount(): Promise { - const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, { - filter: 'status = "active"', + const { totalItems: count } = await pb.collection(COL_USER_METAS).getList(1, 1, { + filter: 'role = "teacher" && status = "active"', }); return count; } diff --git a/002_source/cms/src/db/Customers/GetBlockedCount.tsx b/002_source/cms/src/db/Customers/GetBlockedCount.tsx index 261321c..62e649d 100644 --- a/002_source/cms/src/db/Customers/GetBlockedCount.tsx +++ b/002_source/cms/src/db/Customers/GetBlockedCount.tsx @@ -1,9 +1,9 @@ -import { COL_CUSTOMERS } from '@/constants'; +import { COL_CUSTOMERS, COL_USER_METAS } from '@/constants'; import { pb } from '@/lib/pb'; export default async function GetBlockedCount(): Promise { - const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, { - filter: 'status = "blocked"', + const { totalItems: count } = await pb.collection(COL_USER_METAS).getList(1, 1, { + filter: `role = "teacher" && status = "blocked"`, }); return count; } diff --git a/002_source/cms/src/db/Customers/GetPendingCount.tsx b/002_source/cms/src/db/Customers/GetPendingCount.tsx index d6661ca..eff76fc 100644 --- a/002_source/cms/src/db/Customers/GetPendingCount.tsx +++ b/002_source/cms/src/db/Customers/GetPendingCount.tsx @@ -1,9 +1,9 @@ -import { COL_CUSTOMERS } from '@/constants'; +import { COL_CUSTOMERS, COL_USER_METAS } from '@/constants'; import { pb } from '@/lib/pb'; export default async function GetPendingCount(): Promise { - const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, { - filter: 'status = "pending"', + const { totalItems: count } = await pb.collection(COL_USER_METAS).getList(1, 1, { + filter: 'role = "teacher" && status = "pending"', }); return count; } diff --git a/002_source/cms/src/db/Customers/_GUIDELINES.md b/002_source/cms/src/db/Customers/_GUIDELINES.md index 6515d08..00da7f6 100644 --- a/002_source/cms/src/db/Customers/_GUIDELINES.md +++ b/002_source/cms/src/db/Customers/_GUIDELINES.md @@ -1,6 +1,6 @@ # GUIDELINES -This folder contains drivers for `Customer`/`Customers` records using PocketBase: +This folder contains drivers for `Customer`/`Customers`(Collection ID: pbc_108570809) records using PocketBase: - create (Create.tsx) - read (GetById.tsx) diff --git a/002_source/cms/src/db/Helloworlds/_GUIDELINES.md b/002_source/cms/src/db/Helloworlds/_GUIDELINES.md index 4461080..50910c7 100644 --- a/002_source/cms/src/db/Helloworlds/_GUIDELINES.md +++ b/002_source/cms/src/db/Helloworlds/_GUIDELINES.md @@ -1,6 +1,6 @@ # GUIDELINES -This folder contains test drivers for `Helloworld` records using PocketBase: +This folder contains test drivers for `Helloworld`(Collection ID: pbc_123408445) records using PocketBase: - create (Create.tsx) - read (GetById.tsx) diff --git a/002_source/cms/src/db/LessonCategories/_GUIDELINES.md b/002_source/cms/src/db/LessonCategories/_GUIDELINES.md index 96b23fd..29a23d9 100644 --- a/002_source/cms/src/db/LessonCategories/_GUIDELINES.md +++ b/002_source/cms/src/db/LessonCategories/_GUIDELINES.md @@ -1,6 +1,6 @@ # GUIDELINES -This folder contains drivers for `LessonCategory`/`LessonCategories` records using PocketBase: +This folder contains drivers for `LessonCategory`/`LessonCategories`(Collection ID: pbc_1196309394) records using PocketBase: - create (Create.tsx) - read (GetById.tsx) diff --git a/002_source/cms/src/db/LessonTypes/_GUIDELINES.md b/002_source/cms/src/db/LessonTypes/_GUIDELINES.md index 247a62d..f3dca3f 100644 --- a/002_source/cms/src/db/LessonTypes/_GUIDELINES.md +++ b/002_source/cms/src/db/LessonTypes/_GUIDELINES.md @@ -1,6 +1,6 @@ # GUIDELINES -This folder contains drivers for `LessonType`/`LessonTypes` records using PocketBase: +This folder contains drivers for `LessonType`/`LessonTypes`(Collection ID: pbc_2328411368) records using PocketBase: - create (Create.tsx) - read (GetById.tsx) diff --git a/002_source/cms/src/db/Notifications/_GUIDELINES.md b/002_source/cms/src/db/Notifications/_GUIDELINES.md index 6515d08..b4b995a 100644 --- a/002_source/cms/src/db/Notifications/_GUIDELINES.md +++ b/002_source/cms/src/db/Notifications/_GUIDELINES.md @@ -1,6 +1,8 @@ # GUIDELINES -This folder contains drivers for `Customer`/`Customers` records using PocketBase: +This folder contains drivers for `Notification`/`Notifications` (Collection ID: pbc_977978967) records using PocketBase: + +## File Structure - create (Create.tsx) - read (GetById.tsx) @@ -8,24 +10,66 @@ This folder contains drivers for `Customer`/`Customers` records using PocketBase - count (GetAllCount.tsx, GetActiveCount.tsx, GetBlockedCount.tsx, GetPendingCount.tsx) - misc (Helloworld.tsx) - delete (Delete.tsx) -- list (GetAll.tsx) +- list (GetAll.tsx, GetNotificationByUserId.tsx) -the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` - -## Assumption and Requirements - -- assume `pb` is located in `@/lib/pb` -- no need to handle error in this function, i'll handle it in the caller -- type information defined in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers/type.d.tsx` - -simple template: +## Implementation Template ```typescript import { pb } from '@/lib/pb'; -import { COL_CUSTOMERS } from '@/constants'; +import { COL_NOTIFICATIONS } from '@/constants'; +import type { CreateNotificationProps } from './type.d.ts'; -export async function createCustomer(data: CreateFormProps) { - // ...content - // use direct return of pb.collection (e.g. return pb.collection(xxx)) +export async function createNotification(data: CreateNotificationProps) { + return pb.collection(COL_NOTIFICATIONS).create(data); } ``` + +## Special Considerations + +- User-specific notifications (GetNotificationByUserId.tsx) +- Status transitions (active/pending/blocked) +- Priority levels handling +- Expiration dates +- Bulk operations support + +## Type Definitions + +Key types to use: + +- `NotificationStatus`: active|pending|blocked +- `NotificationPriority`: low|medium|high +- `CreateNotificationProps`: Required fields +- `UpdateNotificationProps`: Partial updates + +## Common Patterns + +```typescript +// Bulk creation example +export async function createBulkNotifications(items: CreateNotificationProps[]) { + return Promise.all(items.map(item => + pb.collection(COL_NOTIFICATIONS).create(item) + )); +} + +// Status update example +export async function markAsRead(id: string) { + return pb.collection(COL_NOTIFICATIONS).update(id, { status: 'read' }); +} +``` + +## Performance Notes + +- Ensure indexes on: user_id, status, created_at +- Consider pagination for large notification lists +- Cache frequently accessed notifications +- Batch operations for mass notifications + +## Testing Guidelines + +Recommended test cases: + +- Single notification creation +- Bulk operations +- Status transitions +- User-specific queries +- Error cases (invalid data, permissions) diff --git a/002_source/cms/src/db/QuizCRCategories/_GUIDELINES.md b/002_source/cms/src/db/QuizCRCategories/_GUIDELINES.md index 49057be..ab91ca2 100644 --- a/002_source/cms/src/db/QuizCRCategories/_GUIDELINES.md +++ b/002_source/cms/src/db/QuizCRCategories/_GUIDELINES.md @@ -1,6 +1,6 @@ # GUIDELINES -This folder contains drivers for `QuizCRCategory`/`QuizCRCategories` records using PocketBase: +This folder contains drivers for `QuizCRCategory`/`QuizCRCategories`(Collection ID: pbc_4061499106) records using PocketBase: - create (Create.tsx) - read (GetById.tsx) diff --git a/002_source/cms/src/db/QuizLPCategories/_GUIDELINES.md b/002_source/cms/src/db/QuizLPCategories/_GUIDELINES.md index 5de26a5..e4f6f8c 100644 --- a/002_source/cms/src/db/QuizLPCategories/_GUIDELINES.md +++ b/002_source/cms/src/db/QuizLPCategories/_GUIDELINES.md @@ -1,6 +1,6 @@ # GUIDELINES -This folder contains drivers for `QuizLPCategory` records using PocketBase: +This folder contains drivers for `QuizLPCategory`(Collection ID: pbc_3639453778) records using PocketBase: - create (Create.tsx) - read (GetById.tsx) diff --git a/002_source/cms/src/db/QuizLPQuestions/_GUIDELINES.md b/002_source/cms/src/db/QuizLPQuestions/_GUIDELINES.md index b1cc44e..6396a6e 100644 --- a/002_source/cms/src/db/QuizLPQuestions/_GUIDELINES.md +++ b/002_source/cms/src/db/QuizLPQuestions/_GUIDELINES.md @@ -1,6 +1,8 @@ # GUIDELINES -This folder contains drivers for `QuizLPQuestion` records using PocketBase: +This folder contains drivers for `QuizLPQuestion`/`QuizLPQuestions` records using PocketBase: + +## File Structure - create (Create.tsx) - read (GetById.tsx) @@ -8,27 +10,72 @@ This folder contains drivers for `QuizLPQuestion` records using PocketBase: - count (GetAllCount.tsx) - delete (Delete.tsx) - list (GetAll.tsx) +- validation (validateQuestion.tsx) -the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` - -## Assumption and Requirements - -- assume `pb` is located in `@/lib/pb` -- no need to handle error in this function, i'll handle it in the caller -- type information defined in `@/db/QuizLPQuestions/type.d.tsx` -- Quiz LP questions require special handling for: - - Answer validation - - Question type checking - - Category association - -simple template: +## Complete Implementation Template ```typescript import { pb } from '@/lib/pb'; import { COL_QUIZ_LP_QUESTIONS } from '@/constants'; +import type { QuizLPQuestion, CreateFormProps } from './type.d.ts'; -export async function createQuizLPQuestion(data: CreateFormProps) { - // ...content - // use direct return of pb.collection (e.g. return pb.collection(COL_QUIZ_LP_QUESTIONS)) +export async function createQuizLPQuestion(data: CreateFormProps): Promise { + return pb.collection(COL_QUIZ_LP_QUESTIONS).create(data); } ``` + +## Question-Specific Handling + +### Answer Validation + +```typescript +export function validateLPQuestionAnswer(question: QuizLPQuestion, answer: string): boolean { + // Implement LP question specific validation + return question.correctAnswer === answer; +} +``` + +### Type Definitions + +```typescript +interface QuizLPQuestion { + id: string; + question: string; + options: string[]; + correctAnswer: string; + categoryId: string; + difficulty: 'easy'|'medium'|'hard'; +} +``` + +## Common Patterns + +```typescript +// Get questions by category +export async function getQuestionsByCategory(categoryId: string) { + return pb.collection(COL_QUIZ_LP_QUESTIONS) + .getFullList({ filter: `categoryId = "${categoryId}"` }); +} + +// Validate before create +export async function createValidatedQuestion(data: CreateFormProps) { + if (!validateQuestionData(data)) throw new Error('Invalid question data'); + return createQuizLPQuestion(data); +} +``` + +## Performance Considerations + +- Add index on: categoryId, difficulty +- Consider pagination for large question sets +- Cache frequently accessed questions + +## Testing Guidelines + +Recommended test cases: + +- Basic CRUD operations +- Answer validation +- Category filtering +- Difficulty level queries +- Error cases (invalid data) diff --git a/002_source/cms/src/db/Students/type.d.ts b/002_source/cms/src/db/Students/type.d.ts new file mode 100644 index 0000000..39e510b --- /dev/null +++ b/002_source/cms/src/db/Students/type.d.ts @@ -0,0 +1,11 @@ +// Student type definitions +export interface Student { + id: string; + name: string; + avatar: string; + email: string; + phone: string; + quota: number; + status: 'active' | 'blocked' | 'pending'; + createdAt: Date; +} diff --git a/002_source/cms/src/db/Users.old/GetAllCount.tsx b/002_source/cms/src/db/Users.old/GetAllCount.tsx new file mode 100644 index 0000000..b82dc88 --- /dev/null +++ b/002_source/cms/src/db/Users.old/GetAllCount.tsx @@ -0,0 +1,15 @@ +// REQ0006 +import { COL_USERS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +export default async function GetAllCount(): Promise { + try { + const result = await pb.collection(`users`).getList(1, 9999, { filter: 'email != ""' }); + const { totalItems: count } = result; + return count; + } catch (error) { + console.error(error); + return -99; + } +} diff --git a/002_source/cms/src/db/Users.old/_GUIDELINES.md b/002_source/cms/src/db/Users.old/_GUIDELINES.md new file mode 100644 index 0000000..17cc639 --- /dev/null +++ b/002_source/cms/src/db/Users.old/_GUIDELINES.md @@ -0,0 +1,30 @@ +# GUIDELINES + +This folder contains drivers for `User`/`Users` records using PocketBase: + +- create (Create.tsx) +- read (GetById.tsx) +- write (Update.tsx) +- count (GetAllCount.tsx) +- delete (Delete.tsx) +- list (GetAll.tsx) + +the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` + +## Assumption and Requirements + +- assume `pb` is located in `@/lib/pb` +- no need to handle error in this function, i'll handle it in the caller +- type information defined in `@/db/Users/type.d.tsx` + +simple template: + +```typescript +import { pb } from '@/lib/pb'; +import { COL_USERS } from '@/constants'; + +export async function createUser(data: CreateFormProps) { + // ...content + // use direct return of pb.collection (e.g. return pb.collection(xxx)) +} +``` diff --git a/002_source/cms/src/db/Vocabularies/_GUIDELINES.md b/002_source/cms/src/db/Vocabularies/_GUIDELINES.md index 96b23fd..c963e14 100644 --- a/002_source/cms/src/db/Vocabularies/_GUIDELINES.md +++ b/002_source/cms/src/db/Vocabularies/_GUIDELINES.md @@ -1,6 +1,8 @@ # GUIDELINES -This folder contains drivers for `LessonCategory`/`LessonCategories` records using PocketBase: +This folder contains drivers for `Vocabulary`/`Vocabularies` records using PocketBase: + +## File Structure - create (Create.tsx) - read (GetById.tsx) @@ -8,23 +10,71 @@ This folder contains drivers for `LessonCategory`/`LessonCategories` records usi - count (GetAllCount.tsx) - delete (Delete.tsx) - list (GetAll.tsx) +- types (type.d.tsx) -the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src` - -## Assumption and Requirements - -- assume `pb` is located in `@/lib/pb` -- no need to handle error in this function, i'll handle it in the caller -- type information defined in `@/db/LessonCategories/type.d.tsx` - -simple template: +## Implementation Template ```typescript import { pb } from '@/lib/pb'; -import { COL_LESSON_CATEGORIES } from '@/constants'; +import { COL_VOCABULARIES } from '@/constants'; +import type { Vocabulary, CreateVocabularyProps } from './type.d.tsx'; -export async function createLessonCategory(data: CreateFormProps) { - // ...content - // use direct return of pb.collection (e.g. return pb.collection(xxx)) +export async function createVocabulary(data: CreateVocabularyProps): Promise { + return pb.collection(COL_VOCABULARIES).create(data); } ``` + +## Vocabulary-Specific Features + +### Field Definitions + +```typescript +interface Vocabulary { + term: string; + definition: string; + language: string; + difficulty: 'beginner'|'intermediate'|'advanced'; + relatedTerms: string[]; // Array of vocabulary IDs +} +``` + +### Common Patterns + +```typescript +// Search by term +export async function searchVocabularies(term: string) { + return pb.collection(COL_VOCABULARIES) + .getFullList({ filter: `term ~ "${term}"` }); +} + +// Get by difficulty level +export async function getVocabulariesByDifficulty(level: string) { + return pb.collection(COL_VOCABULARIES) + .getFullList({ filter: `difficulty = "${level}"` }); +} +``` + +## Type Safety + +```typescript +// Recommended types to use +type CreateVocabularyProps = Omit; +type UpdateVocabularyProps = Partial; +``` + +## Performance Considerations + +- Add indexes on: term, language, difficulty +- Consider full-text search for term/definition fields +- Cache frequently accessed vocabulary items +- Batch operations for bulk imports + +## Testing Guidelines + +Recommended test cases: + +- Basic CRUD operations +- Term search functionality +- Difficulty level filtering +- Related terms validation +- Language-specific queries diff --git a/002_source/cms/src/paths.ts b/002_source/cms/src/paths.ts index 43d71bf..59ca640 100644 --- a/002_source/cms/src/paths.ts +++ b/002_source/cms/src/paths.ts @@ -148,6 +148,14 @@ export const paths = { // edit: (id: string) => `/dashboard/teachers/edit/${id}`, }, }, + users: { + list: '/dashboard/users/list', + create: '/dashboard/users/create', + view: (id: string) => `/dashboard/users/view/${id}`, + // RULES: details is obsoleted, use view instead + details: (id: string) => `/dashboard/teachers/view/${id}`, + edit: (id: string) => `/dashboard/users/edit/${id}`, + }, students: { list: '/dashboard/students/list', create: '/dashboard/students/create', diff --git a/002_source/cms/src/utils/dayjs.ts b/002_source/cms/src/utils/dayjs.ts new file mode 100644 index 0000000..1ed1ad6 --- /dev/null +++ b/002_source/cms/src/utils/dayjs.ts @@ -0,0 +1,18 @@ +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import 'dayjs/locale/zh-cn'; + +dayjs.extend(relativeTime); +dayjs.locale('zh-cn'); + +export const formatDate = (date: Date | string) => { + return dayjs(date).format('YYYY-MM-DD'); +}; + +export const formatDateTime = (date: Date | string) => { + return dayjs(date).format('YYYY-MM-DD HH:mm:ss'); +}; + +export const formatRelativeTime = (date: Date | string) => { + return dayjs(date).fromNow(); +};