Compare commits
13 Commits
develop/cm
...
633a1d3a4c
Author | SHA1 | Date | |
---|---|---|---|
![]() |
633a1d3a4c | ||
![]() |
64b5f89fdf | ||
![]() |
f87dd2c3b1 | ||
![]() |
8b3dfe69e4 | ||
![]() |
375b0a3593 | ||
![]() |
bd907b4dde | ||
![]() |
73a2b2dfb9 | ||
![]() |
2f28d71060 | ||
![]() |
01ce517629 | ||
![]() |
033ca95dfe | ||
![]() |
0fa277c50e | ||
![]() |
e8ded0cb30 | ||
![]() |
cc6e40c9d0 |
@@ -1,15 +0,0 @@
|
||||
# guideline
|
||||
|
||||
- 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
|
||||
|
||||
- example for page can be found in `./src/app/_helloworld/page.tsx`
|
||||
- example for component can be found in `./src/components/_helloworld/index.tsx`
|
||||
|
||||
- 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
|
||||
|
||||
Thanks
|
@@ -1,10 +0,0 @@
|
||||
Hi, i need your help.
|
||||
|
||||
i am working on a react typescript project
|
||||
i will show you part of the code and it is located in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/mf`
|
||||
|
||||
it is copied from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lp`, i want you to help modifing from `lp` to `mf`, the corrosponding word is `lp -> listening practice` and `mf -> matching frenzy`,
|
||||
|
||||
please help to update the `imports`, variables/constants, functions, classes
|
||||
|
||||
thank you
|
39
002_source/cms/_AI_WORKSPACE/components/001_INIT.md
Normal file
39
002_source/cms/_AI_WORKSPACE/components/001_INIT.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# AI GUIDELINE
|
||||
|
||||
## getting started
|
||||
|
||||
Imagine there is a:
|
||||
|
||||
1. developer (provide the modification)
|
||||
2. QA engineer (provide the feedback, and testing)
|
||||
3. software engineer
|
||||
|
||||
software engineer will:
|
||||
|
||||
- conclude and integrate the ideas from developer and QA engineer
|
||||
- make decision to modify the code accordingly.
|
||||
|
||||
no need to reply me what you are going on and your digest in this phase.
|
||||
just reply me "OK" when done
|
||||
|
||||
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
|
||||
|
||||
- `schema.dbml`
|
||||
|
||||
- read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
|
||||
this is file in `dbml` format stating the main database structure
|
||||
|
||||
- `schema.json`
|
||||
|
||||
- read `<base_dir>/002_source/cms/src/db/schema.json`
|
||||
this is the file of current pocketbase schema
|
||||
|
||||
- read `<base_dir>/002_source/cms/src/constants.ts`
|
||||
this is the content of `@/constants`
|
||||
|
||||
- look into the md files in folder `<base_dir>/002_source/cms/_AI_WORKSPACE/001_guideline`
|
||||
|
||||
- directory may contain `repomix-output.xml` file, that is a simple summary of all files inside the directory
|
||||
|
||||
- read, remember and link up the ideas in file stated above,
|
||||
i will tell them the task afterwards
|
22
002_source/cms/_AI_WORKSPACE/components/002_PROMPT.md
Normal file
22
002_source/cms/_AI_WORKSPACE/components/002_PROMPT.md
Normal file
@@ -0,0 +1,22 @@
|
||||
Hi, i need your help.
|
||||
|
||||
i am working on a nextjs react typescript project
|
||||
|
||||
i've already copied the code(and it sub-directories) from
|
||||
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp` (a.k.a. `lp`)
|
||||
to
|
||||
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr` (a.k.a. `cr`)
|
||||
|
||||
i want you to:
|
||||
|
||||
- understand the relations (e.g. `lp` -> `lp_categories`, `lp` -> `COL_QUIZ_LP_CATEGORIES`)
|
||||
- review if any remaining `lp` or `mf` exist in `cr` directory and please help to do replace it to `cr`. (e.g. `COL_QUIZ_LP_CATEGORIES` -> `COL_QUIZ_CR_CATEGORIES` )
|
||||
- please create if you find any missing files/codes/constants
|
||||
|
||||
thank you
|
||||
|
||||
---
|
||||
|
||||
- compare the difference between `lp` and `mf`,
|
||||
- remember the differences and
|
||||
- draft the new type `cr` (e.g. modify the `import` locations, variables, functions, classes, constants name etc.)
|
@@ -1,36 +1,3 @@
|
||||
# AI GUIDELINE
|
||||
|
||||
## getting started
|
||||
|
||||
Imagine there is a software developer and a QA engineer to solve the problems together
|
||||
|
||||
They will:
|
||||
|
||||
no need to reply me what you are going on and your digest in this phase.
|
||||
just reply me "OK" when done
|
||||
|
||||
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
|
||||
|
||||
- `schema.dbml`
|
||||
|
||||
- read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
|
||||
this is file in dbml syntax state the main database
|
||||
|
||||
- `schema.json`
|
||||
|
||||
- read `<base_dir>/002_source/cms/src/db/schema.json`
|
||||
this is the file of live pocketbase schema output
|
||||
|
||||
- read `<base_dir>/002_source/cms/src/constants.ts`
|
||||
this is the content of `@/constants`
|
||||
|
||||
- look into the md files in folder `<base_dir>/002_source/cms/_AI_WORKSPACE/001_guideline`
|
||||
|
||||
- read, remember and link up the ideas in file stated above,
|
||||
i will tell them the task afterwards
|
||||
|
||||
---
|
||||
|
||||
The software engineer will provide solutions,
|
||||
while QA engineer will feedback the opinion.
|
||||
|
37
002_source/cms/_AI_WORKSPACE/db/001_INIT.md
Normal file
37
002_source/cms/_AI_WORKSPACE/db/001_INIT.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# AI GUIDELINE
|
||||
|
||||
## getting started
|
||||
|
||||
Imagine there is a
|
||||
|
||||
1. software developer and a
|
||||
2. QA engineer
|
||||
|
||||
to solve the problems together
|
||||
|
||||
They will:
|
||||
|
||||
no need to reply me what you are going on and your digest in this phase.
|
||||
just reply me "OK" when done
|
||||
|
||||
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
|
||||
|
||||
- `schema.dbml`
|
||||
|
||||
- read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
|
||||
this is file in `dbml` format stating the main database structure
|
||||
|
||||
- `schema.json`
|
||||
|
||||
- read `<base_dir>/002_source/cms/src/db/schema.json`
|
||||
this is the file of current pocketbase schema
|
||||
|
||||
- read `<base_dir>/002_source/cms/src/constants.ts`
|
||||
this is the content of `@/constants`
|
||||
|
||||
- look into the md files in folder `<base_dir>/002_source/cms/_AI_WORKSPACE/001_guideline`
|
||||
|
||||
- directory may contain `repomix-output.xml` file, that is a simple summary of all files inside the directory
|
||||
|
||||
- read, remember and link up the ideas in file stated above,
|
||||
i will tell them the task afterwards
|
22
002_source/cms/_AI_WORKSPACE/db/002_PROMPT.md
Normal file
22
002_source/cms/_AI_WORKSPACE/db/002_PROMPT.md
Normal file
@@ -0,0 +1,22 @@
|
||||
Hi, i need your help.
|
||||
|
||||
i am working on a nextjs react typescript project
|
||||
|
||||
please read the code in directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPQuestions` (denoted `QuizLPQuestions`)
|
||||
|
||||
i want you to:
|
||||
|
||||
- list files from directory `QuizLPQuestions` and use them as template
|
||||
- understand the relations (e.g. `lp` -> `lp_categories`, `lp` -> `COL_QUIZ_LP_CATEGORIES`)
|
||||
- review if any remaining `lp` or `mf` exist in `cr` directory and please help to do replace it to `cr`. (e.g. `COL_QUIZ_LP_CATEGORIES` -> `COL_QUIZ_CR_CATEGORIES` )
|
||||
- please create if you find any missing files/codes/constants
|
||||
- using template, create the similar code for `cr` named `QuizCRCategories`
|
||||
|
||||
thank you
|
||||
|
||||
---
|
||||
|
||||
- I proofed the code is working already, what you need to do is refactoring of the `variables`, `functions`, `classes`, `constants` (`lp` -> `cr`)
|
||||
- compare the difference between `lp` and `mf`,
|
||||
- remember the differences and
|
||||
- draft the new type `cr` (e.g. modify the `import` locations, variables, functions, classes, constants name etc.)
|
22
002_source/cms/_AI_WORKSPACE/db/002_PROMPT_categories.md
Normal file
22
002_source/cms/_AI_WORKSPACE/db/002_PROMPT_categories.md
Normal file
@@ -0,0 +1,22 @@
|
||||
Hi, i need your help.
|
||||
|
||||
i am working on a nextjs react typescript project
|
||||
|
||||
please read the code in directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPCategories` (denoted `QuizLPCategories`)
|
||||
|
||||
i want you to:
|
||||
|
||||
- list files from directory `QuizLPCategories` and use them as template
|
||||
- understand the relations (e.g. `lp` -> `lp_categories`, `lp` -> `COL_QUIZ_LP_CATEGORIES`)
|
||||
- review if any remaining `lp` or `mf` exist in `cr` directory and please help to do replace it to `cr`. (e.g. `COL_QUIZ_LP_CATEGORIES` -> `COL_QUIZ_CR_CATEGORIES` )
|
||||
- please create if you find any missing files/codes/constants
|
||||
- using template, create the similar code for `cr` named `QuizCRCategories`
|
||||
|
||||
thank you
|
||||
|
||||
---
|
||||
|
||||
- I proofed the code is working already, what you need to do is refactoring of the `variables`, `functions`, `classes`, `constants` (`lp` -> `cr`)
|
||||
- compare the difference between `lp` and `mf`,
|
||||
- remember the differences and
|
||||
- draft the new type `cr` (e.g. modify the `import` locations, variables, functions, classes, constants name etc.)
|
22
002_source/cms/_AI_WORKSPACE/db/002_PROMPT_questions.md
Normal file
22
002_source/cms/_AI_WORKSPACE/db/002_PROMPT_questions.md
Normal file
@@ -0,0 +1,22 @@
|
||||
Hi, i need your help.
|
||||
|
||||
i am working on a nextjs react typescript project
|
||||
|
||||
please read the code in directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPQuestions` (denoted `QuizLPQuestions`)
|
||||
|
||||
i want you to:
|
||||
|
||||
- list files from directory `QuizLPQuestions` and use them as template
|
||||
- understand the relations (e.g. `lp` -> `lp_Questions`, `lp` -> `COL_QUIZ_LP_QUESTIONS`)
|
||||
- review if any remaining `lp` or `mf` exist in `cr` directory and please help to do replace it to `cr`. (e.g. `COL_QUIZ_LP_QUESTIONS` -> `COL_QUIZ_CR_Questions` )
|
||||
- please create if you find any missing files/codes/constants
|
||||
- using template, create the similar code for `cr` named `QuizCRQuestions`
|
||||
|
||||
thank you
|
||||
|
||||
---
|
||||
|
||||
- I proofed the code is working already, what you need to do is refactoring of the `variables`, `functions`, `classes`, `constants` (`lp` -> `cr`)
|
||||
- compare the difference between `lp` and `mf`,
|
||||
- remember the differences and
|
||||
- draft the new type `cr` (e.g. modify the `import` locations, variables, functions, classes, constants name etc.)
|
66
002_source/cms/_AI_WORKSPACE/db/DB_PROMPT.MD
Normal file
66
002_source/cms/_AI_WORKSPACE/db/DB_PROMPT.MD
Normal file
@@ -0,0 +1,66 @@
|
||||
The software engineer will provide solutions,
|
||||
while QA engineer will feedback the opinion.
|
||||
|
||||
this is now not in debug phase,
|
||||
so, no need to reply me what they are going on or their insight throught the prompt.
|
||||
just reply me "OK" when done
|
||||
|
||||
---
|
||||
|
||||
clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it
|
||||
|
||||
please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden`
|
||||
|
||||
well done !, please proceed to another request
|
||||
|
||||
working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db`
|
||||
|
||||
according information from `schema.json`, get the collection of `Students`
|
||||
|
||||
pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content
|
||||
|
||||
when you draft coding, review file and append with `.tsx.draft`
|
||||
|
||||
---
|
||||
|
||||
- this is part of react typescript project, with pocketbase
|
||||
- `schema.dbml`, describe the collections(tables)
|
||||
- folder `LessonCategories`, the correct references
|
||||
- folder `LessonTypes`, the correct references
|
||||
- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006`
|
||||
- do not read root directory, assume it is a fresh copy of nextjs project is ok
|
||||
|
||||
## instruction
|
||||
|
||||
- break the questions into smaller parts
|
||||
- review file append with `.draft`, see if the content aligned with the correct references
|
||||
- read and understand `dbml` file
|
||||
- lookup the every folder
|
||||
|
||||
## tasks
|
||||
|
||||
Thanks
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
please revise
|
||||
|
||||
please revise
|
||||
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory`
|
||||
|
||||
to the collection `QuizLPCategories` align the dbml file in the previous prompt
|
||||
|
||||
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx`
|
||||
|
||||
to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory`
|
||||
|
||||
---
|
||||
|
||||
the constants file (`@/constants`) was `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts`
|
||||
|
||||
please help to fix the `tsx` files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizMFCategories`,
|
||||
the `COL` constants is wrongly used, it should refer to `COL_QUIZ_MF_CATEGORIES`. thanks
|
||||
|
||||
please update the `COL_XXXX` TO COL_MF_CATEGORIES
|
38
002_source/cms/_AI_WORKSPACE/get_summary/001_INIT.md
Normal file
38
002_source/cms/_AI_WORKSPACE/get_summary/001_INIT.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# AI GUIDELINE
|
||||
|
||||
## getting started
|
||||
|
||||
Imagine there is a
|
||||
|
||||
1. software developer and a
|
||||
2. QA engineer
|
||||
3. technical writer
|
||||
|
||||
to solve the problems together
|
||||
|
||||
They will:
|
||||
|
||||
no need to reply me what you are going on and your digest in this phase.
|
||||
just reply me "OK" when done
|
||||
|
||||
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
|
||||
|
||||
- `schema.dbml`
|
||||
|
||||
- read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
|
||||
this is file in `dbml` format stating the main database structure
|
||||
|
||||
- `schema.json`
|
||||
|
||||
- read `<base_dir>/002_source/cms/src/db/schema.json`
|
||||
this is the file of current pocketbase schema
|
||||
|
||||
- read `<base_dir>/002_source/cms/src/constants.ts`
|
||||
this is the content of `@/constants`
|
||||
|
||||
- look into the md files in folder `<base_dir>/002_source/cms/_AI_WORKSPACE/001_guideline`
|
||||
|
||||
- directory may contain `repomix-output.xml` file, that is a simple summary of all files inside the directory
|
||||
|
||||
- read, remember and link up the ideas in file stated above,
|
||||
i will tell them the task afterwards
|
15
002_source/cms/_AI_WORKSPACE/get_summary/002_PROMPT.md
Normal file
15
002_source/cms/_AI_WORKSPACE/get_summary/002_PROMPT.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Hi, i need your help.
|
||||
|
||||
## background
|
||||
|
||||
i am working on a nextjs react typescript project
|
||||
|
||||
## task
|
||||
|
||||
i want you to summarize the code, write it to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories/_PROMPT.md`
|
||||
|
||||
## steps
|
||||
|
||||
1. list all `tsx` files from directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories`
|
||||
2. take a look to the `tsx` file listed. try to understand what's going on (e.g. it is a component for `cr-categories`, it is a table)
|
||||
3. write your result to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories/_PROMPT.md`
|
@@ -0,0 +1,17 @@
|
||||
Hi, i need your help.
|
||||
|
||||
## background
|
||||
|
||||
i am working on a nextjs react typescript project
|
||||
|
||||
## task
|
||||
|
||||
i want you to summarize the code, write it to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/questions/_SUMMARY.md`
|
||||
|
||||
## steps
|
||||
|
||||
1. list all `tsx` files from directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/questions`
|
||||
2. read every `tsx` files listed. try to understand what's going on (e.g. it is a component for `cr-questions`, it is a table)
|
||||
3. read file `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/questions/_SUMMARY.md`.
|
||||
4. make use of `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/questions/_SUMMARY.md` as template, update the content and create a new summary
|
||||
5. write your new summary to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/questions/_SUMMARY.md`
|
@@ -0,0 +1,17 @@
|
||||
Hi, i need your help.
|
||||
|
||||
## background
|
||||
|
||||
i am working on a nextjs react typescript project
|
||||
|
||||
## task
|
||||
|
||||
i want you to summarize the code, write it to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/categories/_SUMMARY.md`
|
||||
|
||||
## steps
|
||||
|
||||
1. list all `tsx` files from directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/categories`
|
||||
2. read every `tsx` files listed. try to understand what's going on (e.g. it is a component for `cr-categories`, it is a table)
|
||||
3. read file `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories/_SUMMARY.md`.
|
||||
4. make use of `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories/_SUMMARY.md` as template, update the content and create a new summary
|
||||
5. write your new summary to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/categories/_SUMMARY.md`
|
66
002_source/cms/_AI_WORKSPACE/get_summary/DB_PROMPT.MD
Normal file
66
002_source/cms/_AI_WORKSPACE/get_summary/DB_PROMPT.MD
Normal file
@@ -0,0 +1,66 @@
|
||||
The software engineer will provide solutions,
|
||||
while QA engineer will feedback the opinion.
|
||||
|
||||
this is now not in debug phase,
|
||||
so, no need to reply me what they are going on or their insight throught the prompt.
|
||||
just reply me "OK" when done
|
||||
|
||||
---
|
||||
|
||||
clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it
|
||||
|
||||
please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden`
|
||||
|
||||
well done !, please proceed to another request
|
||||
|
||||
working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db`
|
||||
|
||||
according information from `schema.json`, get the collection of `Students`
|
||||
|
||||
pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content
|
||||
|
||||
when you draft coding, review file and append with `.tsx.draft`
|
||||
|
||||
---
|
||||
|
||||
- this is part of react typescript project, with pocketbase
|
||||
- `schema.dbml`, describe the collections(tables)
|
||||
- folder `LessonCategories`, the correct references
|
||||
- folder `LessonTypes`, the correct references
|
||||
- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006`
|
||||
- do not read root directory, assume it is a fresh copy of nextjs project is ok
|
||||
|
||||
## instruction
|
||||
|
||||
- break the questions into smaller parts
|
||||
- review file append with `.draft`, see if the content aligned with the correct references
|
||||
- read and understand `dbml` file
|
||||
- lookup the every folder
|
||||
|
||||
## tasks
|
||||
|
||||
Thanks
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
please revise
|
||||
|
||||
please revise
|
||||
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory`
|
||||
|
||||
to the collection `QuizLPCategories` align the dbml file in the previous prompt
|
||||
|
||||
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx`
|
||||
|
||||
to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory`
|
||||
|
||||
---
|
||||
|
||||
the constants file (`@/constants`) was `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts`
|
||||
|
||||
please help to fix the `tsx` files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizMFCategories`,
|
||||
the `COL` constants is wrongly used, it should refer to `COL_QUIZ_MF_CATEGORIES`. thanks
|
||||
|
||||
please update the `COL_XXXX` TO COL_MF_CATEGORIES
|
37
002_source/cms/_AI_WORKSPACE/pages/001_INIT.md
Normal file
37
002_source/cms/_AI_WORKSPACE/pages/001_INIT.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# AI GUIDELINE
|
||||
|
||||
## getting started
|
||||
|
||||
Imagine there is a
|
||||
|
||||
1. software developer and a
|
||||
2. QA engineer
|
||||
|
||||
to solve the problems together
|
||||
|
||||
They will:
|
||||
|
||||
no need to reply me what you are going on and your digest in this phase.
|
||||
just reply me "OK" when done
|
||||
|
||||
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
|
||||
|
||||
- `schema.dbml`
|
||||
|
||||
- read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
|
||||
this is file in `dbml` format stating the main database structure
|
||||
|
||||
- `schema.json`
|
||||
|
||||
- read `<base_dir>/002_source/cms/src/db/schema.json`
|
||||
this is the file of current pocketbase schema
|
||||
|
||||
- read `<base_dir>/002_source/cms/src/constants.ts`
|
||||
this is the content of `@/constants`
|
||||
|
||||
- look into the md files in folder `<base_dir>/002_source/cms/_AI_WORKSPACE/001_guideline`
|
||||
|
||||
- directory may contain `repomix-output.xml` file, that is a simple summary of all files inside the directory
|
||||
|
||||
- read, remember and link up the ideas in file stated above,
|
||||
i will tell them the task afterwards
|
25
002_source/cms/_AI_WORKSPACE/pages/002_PROMPT.md
Normal file
25
002_source/cms/_AI_WORKSPACE/pages/002_PROMPT.md
Normal file
@@ -0,0 +1,25 @@
|
||||
Hi, i need your help.
|
||||
|
||||
i am working on a nextjs react typescript project
|
||||
|
||||
i've already copied the code(and it sub-directories) from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lp` (a.k.a. `lp`) to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/cr` (a.k.a. `cr`)
|
||||
|
||||
i want you to:
|
||||
|
||||
- list files from directory
|
||||
|
||||
- `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lp`
|
||||
- `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/cr`
|
||||
|
||||
- understand the relations (e.g. `lp` -> `lp_categories`, `lp` -> `COL_QUIZ_LP_CATEGORIES`)
|
||||
- review if any remaining `lp` or `mf` exist in `cr` directory and please help to do replace it to `cr`. (e.g. `COL_QUIZ_LP_CATEGORIES` -> `COL_QUIZ_CR_CATEGORIES` )
|
||||
- please create if you find any missing files/codes/constants
|
||||
|
||||
thank you
|
||||
|
||||
---
|
||||
|
||||
- I proofed the code is working already, what you need to do is refactoring of the `variables`, `functions`, `classes`, `constants` (`lp` -> `cr`)
|
||||
- compare the difference between `lp` and `mf`,
|
||||
- remember the differences and
|
||||
- draft the new type `cr` (e.g. modify the `import` locations, variables, functions, classes, constants name etc.)
|
66
002_source/cms/_AI_WORKSPACE/pages/DB_PROMPT.MD
Normal file
66
002_source/cms/_AI_WORKSPACE/pages/DB_PROMPT.MD
Normal file
@@ -0,0 +1,66 @@
|
||||
The software engineer will provide solutions,
|
||||
while QA engineer will feedback the opinion.
|
||||
|
||||
this is now not in debug phase,
|
||||
so, no need to reply me what they are going on or their insight throught the prompt.
|
||||
just reply me "OK" when done
|
||||
|
||||
---
|
||||
|
||||
clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it
|
||||
|
||||
please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden`
|
||||
|
||||
well done !, please proceed to another request
|
||||
|
||||
working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db`
|
||||
|
||||
according information from `schema.json`, get the collection of `Students`
|
||||
|
||||
pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content
|
||||
|
||||
when you draft coding, review file and append with `.tsx.draft`
|
||||
|
||||
---
|
||||
|
||||
- this is part of react typescript project, with pocketbase
|
||||
- `schema.dbml`, describe the collections(tables)
|
||||
- folder `LessonCategories`, the correct references
|
||||
- folder `LessonTypes`, the correct references
|
||||
- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006`
|
||||
- do not read root directory, assume it is a fresh copy of nextjs project is ok
|
||||
|
||||
## instruction
|
||||
|
||||
- break the questions into smaller parts
|
||||
- review file append with `.draft`, see if the content aligned with the correct references
|
||||
- read and understand `dbml` file
|
||||
- lookup the every folder
|
||||
|
||||
## tasks
|
||||
|
||||
Thanks
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
please revise
|
||||
|
||||
please revise
|
||||
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory`
|
||||
|
||||
to the collection `QuizLPCategories` align the dbml file in the previous prompt
|
||||
|
||||
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx`
|
||||
|
||||
to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory`
|
||||
|
||||
---
|
||||
|
||||
the constants file (`@/constants`) was `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts`
|
||||
|
||||
please help to fix the `tsx` files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizMFCategories`,
|
||||
the `COL` constants is wrongly used, it should refer to `COL_QUIZ_MF_CATEGORIES`. thanks
|
||||
|
||||
please update the `COL_XXXX` TO COL_MF_CATEGORIES
|
@@ -22,8 +22,8 @@
|
||||
"test": "jest --watch",
|
||||
"update_doc": "cd 001_documentation/Requirements && node update_req_index.js",
|
||||
"update_doc:w": "pnpx nodemon --ext md --exec \"pnpm run update_doc\"",
|
||||
"update_repomix": "npx repomix",
|
||||
"update_repomix:w": "pnpx nodemon --delay 3 --exec \"pnpx repomix\"",
|
||||
"update_repomix": "node ./scripts/update_repomix.js",
|
||||
"update_repomix:w": "pnpx nodemon --delay 3 --exec \"npm run update_repomix\"",
|
||||
"clean": "rm -rf node_modules && rm -rf .next"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@@ -28,7 +28,9 @@ const config = {
|
||||
'',
|
||||
'^[./]',
|
||||
],
|
||||
plugins: ['@ianvs/prettier-plugin-sort-imports'],
|
||||
plugins: [
|
||||
// '@ianvs/prettier-plugin-sort-imports'
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.tsx'],
|
||||
|
7
002_source/cms/scripts/005_update_repomix.sh
Executable file
7
002_source/cms/scripts/005_update_repomix.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
node ./scripts/update_repomix.js
|
||||
|
||||
echo "done"
|
@@ -1,19 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
set -x
|
||||
|
||||
# ls **/.next
|
||||
# ls **/.pnpm
|
||||
# ls **/node_modules
|
||||
ls **/*.bak
|
||||
ls **/*draft
|
||||
ls **/*.del
|
||||
ls **/*.plan
|
||||
ls **/*Zone*
|
||||
|
||||
set -e
|
||||
# set -e
|
||||
|
||||
read -p "Press [Enter] key to clean directories..."
|
||||
# read -p "Press [Enter] key to clean directories..."
|
||||
|
||||
# rm -rf **/.next
|
||||
# rm -rf **/.pnpm
|
||||
# rm -rf **/node_modules
|
||||
rm -rf **/*Zone*
|
||||
# # rm -rf **/.next
|
||||
# # rm -rf **/.pnpm
|
||||
# # rm -rf **/node_modules
|
||||
# rm -rf **/*Zone*
|
||||
|
||||
echo "clean done"
|
||||
|
37
002_source/cms/scripts/update_repomix.js
Normal file
37
002_source/cms/scripts/update_repomix.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const exec = require('child_process').exec;
|
||||
|
||||
let directories = [
|
||||
//
|
||||
'./src/db',
|
||||
'src/app/dashboard/lp',
|
||||
'src/app/dashboard/mf',
|
||||
'src/app/dashboard/cr',
|
||||
'src/components/dashboard/lp',
|
||||
'src/components/dashboard/mf',
|
||||
'src/components/dashboard/cr',
|
||||
'src/app/dashboard/Sample',
|
||||
].map((directory) => {
|
||||
return `cd ${directory} && pnpx repomix`;
|
||||
});
|
||||
|
||||
Promise.all(
|
||||
directories.map(
|
||||
(directory) =>
|
||||
new Promise((resolve, reject) => {
|
||||
exec(directory, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
console.log(stdout);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
console.log('done');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
@@ -12,9 +12,9 @@ import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { Address } from '@/types/Address';
|
||||
import { ShippingAddress } from '@/components/dashboard/lp_categories/shipping-address';
|
||||
import { ShippingAddress } from '@/components/dashboard/lp/categories/shipping-address';
|
||||
|
||||
import { SampleAddresses } from '../SampleAddresses';
|
||||
import { SampleAddresses } from './SampleAddresses';
|
||||
|
||||
export default function SampleAddressCard(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
@@ -22,7 +22,10 @@ export default function SampleAddressCard(): React.JSX.Element {
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button color="secondary" startIcon={<PlusIcon />}>
|
||||
<Button
|
||||
color="secondary"
|
||||
startIcon={<PlusIcon />}
|
||||
>
|
||||
{t('list.add')}
|
||||
</Button>
|
||||
}
|
||||
@@ -34,9 +37,16 @@ export default function SampleAddressCard(): React.JSX.Element {
|
||||
title={t('list.shipping-addresses')}
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid container spacing={3}>
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
>
|
||||
{(SampleAddresses satisfies Address[]).map((address) => (
|
||||
<Grid key={address.id} md={6} xs={12}>
|
||||
<Grid
|
||||
key={address.id}
|
||||
md={6}
|
||||
xs={12}
|
||||
>
|
||||
<ShippingAddress address={address} />
|
||||
</Grid>
|
||||
))}
|
||||
|
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PropertyItem } from '@/components/core/property-item';
|
||||
import { PropertyList } from '@/components/core/property-list';
|
||||
import { Payments } from '@/components/dashboard/lp_categories/payments';
|
||||
import { Payments } from '@/components/dashboard/lp/categories/payments';
|
||||
|
||||
import { SamplePayments } from './SamplePayments';
|
||||
|
||||
@@ -21,11 +21,19 @@ export default function SamplePaymentCard(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Payments ordersValue={2069.48} payments={SamplePayments} refundsValue={324.5} totalOrders={5} />
|
||||
<Payments
|
||||
ordersValue={2069.48}
|
||||
payments={SamplePayments}
|
||||
refundsValue={324.5}
|
||||
totalOrders={5}
|
||||
/>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
|
||||
<Button
|
||||
color="secondary"
|
||||
startIcon={<PencilSimpleIcon />}
|
||||
>
|
||||
{t('list.edit')}
|
||||
</Button>
|
||||
}
|
||||
@@ -37,8 +45,14 @@ export default function SamplePaymentCard(): React.JSX.Element {
|
||||
title={t('list.billing-details')}
|
||||
/>
|
||||
<CardContent>
|
||||
<Card sx={{ borderRadius: 1 }} variant="outlined">
|
||||
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}>
|
||||
<Card
|
||||
sx={{ borderRadius: 1 }}
|
||||
variant="outlined"
|
||||
>
|
||||
<PropertyList
|
||||
divider={<Divider />}
|
||||
sx={{ '--PropertyItem-padding': '16px' }}
|
||||
>
|
||||
{(
|
||||
[
|
||||
{ key: t('Credit card'), value: '**** 4142' },
|
||||
@@ -50,7 +64,11 @@ export default function SamplePaymentCard(): React.JSX.Element {
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
||||
<PropertyItem
|
||||
key={item.key}
|
||||
name={item.key}
|
||||
value={item.value}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
@@ -27,11 +27,17 @@ export default function SampleSecurityCard(): React.JSX.Element {
|
||||
<CardContent>
|
||||
<Stack spacing={1}>
|
||||
<div>
|
||||
<Button color="error" variant="contained">
|
||||
<Button
|
||||
color="error"
|
||||
variant="contained"
|
||||
>
|
||||
{t('Delete account')}
|
||||
</Button>
|
||||
</div>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
variant="body2"
|
||||
>
|
||||
{t('a-deleted-customer-cannot-be-restored-all-data-will-be-permanently-removed')}
|
||||
</Typography>
|
||||
</Stack>
|
558
002_source/cms/src/app/dashboard/Sample/repomix-output.xml
Normal file
558
002_source/cms/src/app/dashboard/Sample/repomix-output.xml
Normal file
@@ -0,0 +1,558 @@
|
||||
This file is a merged representation of the entire codebase, combined into a single document by Repomix.
|
||||
|
||||
<file_summary>
|
||||
This section contains a summary of this file.
|
||||
|
||||
<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.
|
||||
</purpose>
|
||||
|
||||
<file_format>
|
||||
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
|
||||
</file_format>
|
||||
|
||||
<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.
|
||||
</usage_guidelines>
|
||||
|
||||
<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)
|
||||
</notes>
|
||||
|
||||
<additional_info>
|
||||
|
||||
</additional_info>
|
||||
|
||||
</file_summary>
|
||||
|
||||
<directory_structure>
|
||||
AddressCard/
|
||||
index.tsx
|
||||
SampleAddresses.tsx
|
||||
BasicDetailCard/
|
||||
index.tsx
|
||||
Notifications/
|
||||
index.tsx
|
||||
type.d.ts
|
||||
PaymentCard/
|
||||
index.tsx
|
||||
SamplePayments.tsx
|
||||
SecurityCard/
|
||||
index.tsx
|
||||
TitleCard/
|
||||
index.tsx
|
||||
Helloworld.tsx
|
||||
</directory_structure>
|
||||
|
||||
<files>
|
||||
This section contains the contents of the repository's files.
|
||||
|
||||
<file path="AddressCard/index.tsx">
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
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 Grid from '@mui/material/Unstable_Grid2';
|
||||
import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { Address } from '@/types/Address';
|
||||
import { ShippingAddress } from '@/components/dashboard/lp/categories/shipping-address';
|
||||
|
||||
import { SampleAddresses } from './SampleAddresses';
|
||||
|
||||
export default function SampleAddressCard(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button
|
||||
color="secondary"
|
||||
startIcon={<PlusIcon />}
|
||||
>
|
||||
{t('list.add')}
|
||||
</Button>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<HouseIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('list.shipping-addresses')}
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
>
|
||||
{(SampleAddresses satisfies Address[]).map((address) => (
|
||||
<Grid
|
||||
key={address.id}
|
||||
md={6}
|
||||
xs={12}
|
||||
>
|
||||
<ShippingAddress address={address} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
</file>
|
||||
|
||||
<file path="AddressCard/SampleAddresses.tsx">
|
||||
'use client';
|
||||
|
||||
import type { Address } from '@/types/Address';
|
||||
|
||||
export const SampleAddresses: Address[] = [
|
||||
{
|
||||
id: 'ADR-001',
|
||||
country: 'United States',
|
||||
state: 'Michigan',
|
||||
city: 'Lansing',
|
||||
zipCode: '48933',
|
||||
street: '480 Haven Lane',
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
id: 'ADR-002',
|
||||
country: 'United States',
|
||||
state: 'Missouri',
|
||||
city: 'Springfield',
|
||||
zipCode: '65804',
|
||||
street: '4807 Lighthouse Drive',
|
||||
},
|
||||
];
|
||||
</file>
|
||||
|
||||
<file path="BasicDetailCard/index.tsx">
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
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 LinearProgress from '@mui/material/LinearProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
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';
|
||||
|
||||
export default function BasicDetailCard({
|
||||
lpCatId,
|
||||
handleEditClick,
|
||||
}: {
|
||||
lpCatId: string;
|
||||
handleEditClick: () => void;
|
||||
}): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleEditClick();
|
||||
}}
|
||||
>
|
||||
<PencilSimpleIcon />
|
||||
</IconButton>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('list.basic-details')}
|
||||
/>
|
||||
<PropertyList
|
||||
divider={<Divider />}
|
||||
orientation="vertical"
|
||||
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
||||
>
|
||||
{(
|
||||
[
|
||||
{
|
||||
key: 'Customer ID',
|
||||
value: (
|
||||
<Chip
|
||||
label="USR-001"
|
||||
size="small"
|
||||
variant="soft"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ key: 'Name', value: 'Miron Vitold' },
|
||||
{ key: 'Email', value: 'miron.vitold@domain.com' },
|
||||
{ key: 'Phone', value: '(425) 434-5535' },
|
||||
{ key: 'Company', value: 'Devias IO' },
|
||||
{
|
||||
key: 'Quota',
|
||||
value: (
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{ alignItems: 'center' }}
|
||||
>
|
||||
<LinearProgress
|
||||
sx={{ flex: '1 1 auto' }}
|
||||
value={50}
|
||||
variant="determinate"
|
||||
/>
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
variant="body2"
|
||||
>
|
||||
50%
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem
|
||||
key={item.key}
|
||||
name={item.key}
|
||||
value={item.value}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
</file>
|
||||
|
||||
<file path="Notifications/index.tsx">
|
||||
'use client';
|
||||
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
|
||||
import type { Notification } from './type';
|
||||
|
||||
export const SampleNotifications: Notification[] = [
|
||||
{
|
||||
id: 'EV-002',
|
||||
type: 'Refund request approved',
|
||||
status: 'pending',
|
||||
createdAt: dayjs().subtract(34, 'minute').subtract(5, 'hour').subtract(3, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'EV-001',
|
||||
type: 'Order confirmation',
|
||||
status: 'delivered',
|
||||
createdAt: dayjs().subtract(49, 'minute').subtract(11, 'hour').subtract(4, 'day').toDate(),
|
||||
},
|
||||
];
|
||||
</file>
|
||||
|
||||
<file path="Notifications/type.d.ts">
|
||||
export interface Notification {
|
||||
id: string;
|
||||
type: string;
|
||||
status: 'delivered' | 'pending' | 'failed';
|
||||
createdAt: Date;
|
||||
}
|
||||
</file>
|
||||
|
||||
<file path="PaymentCard/index.tsx">
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
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 Divider from '@mui/material/Divider';
|
||||
import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard';
|
||||
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PropertyItem } from '@/components/core/property-item';
|
||||
import { PropertyList } from '@/components/core/property-list';
|
||||
import { Payments } from '@/components/dashboard/lp/categories/payments';
|
||||
|
||||
import { SamplePayments } from './SamplePayments';
|
||||
|
||||
export default function SamplePaymentCard(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Payments
|
||||
ordersValue={2069.48}
|
||||
payments={SamplePayments}
|
||||
refundsValue={324.5}
|
||||
totalOrders={5}
|
||||
/>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button
|
||||
color="secondary"
|
||||
startIcon={<PencilSimpleIcon />}
|
||||
>
|
||||
{t('list.edit')}
|
||||
</Button>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('list.billing-details')}
|
||||
/>
|
||||
<CardContent>
|
||||
<Card
|
||||
sx={{ borderRadius: 1 }}
|
||||
variant="outlined"
|
||||
>
|
||||
<PropertyList
|
||||
divider={<Divider />}
|
||||
sx={{ '--PropertyItem-padding': '16px' }}
|
||||
>
|
||||
{(
|
||||
[
|
||||
{ key: t('Credit card'), value: '**** 4142' },
|
||||
{ key: t('Country'), value: t('United States') },
|
||||
{ key: t('State'), value: t('Michigan') },
|
||||
{ key: t('City'), value: t('Southfield') },
|
||||
{ key: t('Address'), value: t('Address') },
|
||||
{ key: t('Tax ID'), value: t('Tax ID') },
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem
|
||||
key={item.key}
|
||||
name={item.key}
|
||||
value={item.value}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
</file>
|
||||
|
||||
<file path="PaymentCard/SamplePayments.tsx">
|
||||
'use client';
|
||||
|
||||
// import { dayjs } from 'dayjs';
|
||||
import type { Payment } from '@/types/Payment';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
|
||||
export const SamplePayments: Payment[] = [
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 500,
|
||||
invoiceId: 'INV-005',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(5, 'minute').subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 324.5,
|
||||
invoiceId: 'INV-004',
|
||||
status: 'refunded',
|
||||
createdAt: dayjs().subtract(21, 'minute').subtract(2, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 746.5,
|
||||
invoiceId: 'INV-003',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(7, 'minute').subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 56.89,
|
||||
invoiceId: 'INV-002',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(48, 'minute').subtract(4, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 541.59,
|
||||
invoiceId: 'INV-001',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(31, 'minute').subtract(5, 'hour').toDate(),
|
||||
},
|
||||
];
|
||||
</file>
|
||||
|
||||
<file path="SecurityCard/index.tsx">
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
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 Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function SampleSecurityCard(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('list.security')}
|
||||
/>
|
||||
<CardContent>
|
||||
<Stack spacing={1}>
|
||||
<div>
|
||||
<Button
|
||||
color="error"
|
||||
variant="contained"
|
||||
>
|
||||
{t('Delete account')}
|
||||
</Button>
|
||||
</div>
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
variant="body2"
|
||||
>
|
||||
{t('a-deleted-customer-cannot-be-restored-all-data-will-be-permanently-removed')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
</file>
|
||||
|
||||
<file path="TitleCard/index.tsx">
|
||||
'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';
|
||||
|
||||
export default function SampleTitleCard(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{ alignItems: 'center', flex: '1 1 auto' }}
|
||||
>
|
||||
<Avatar
|
||||
src="/assets/avatar-1.png"
|
||||
sx={{ '--Avatar-size': '64px' }}
|
||||
>
|
||||
empty
|
||||
</Avatar>
|
||||
<div>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
|
||||
>
|
||||
<Typography variant="h4">{t('list.customer-name')}</Typography>
|
||||
<Chip
|
||||
icon={
|
||||
<CheckCircleIcon
|
||||
color="var(--mui-palette-success-main)"
|
||||
weight="fill"
|
||||
/>
|
||||
}
|
||||
label={t('list.active')}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Stack>
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
variant="body1"
|
||||
>
|
||||
{t('list.customer-email')}
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<div>
|
||||
<Button
|
||||
endIcon={<CaretDownIcon />}
|
||||
variant="contained"
|
||||
>
|
||||
{t('list.action')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
</file>
|
||||
|
||||
<file path="Helloworld.tsx">
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
|
||||
|
||||
function Page(): React.JSX.Element {
|
||||
React.useLayoutEffect(() => {
|
||||
console.log('helloworld');
|
||||
}, []);
|
||||
|
||||
return <>helloworld</>;
|
||||
}
|
||||
|
||||
export default Page;
|
||||
</file>
|
||||
|
||||
</files>
|
@@ -0,0 +1,79 @@
|
||||
'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 { CrCategory } from '@/components/dashboard/cr/categories/type';
|
||||
|
||||
export default function BasicDetailCard({
|
||||
lpModel: model,
|
||||
handleEditClick,
|
||||
}: {
|
||||
lpModel: CrCategory;
|
||||
handleEditClick: () => void;
|
||||
}): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleEditClick();
|
||||
}}
|
||||
>
|
||||
<PencilSimpleIcon />
|
||||
</IconButton>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('list.basic-details')}
|
||||
/>
|
||||
<PropertyList
|
||||
divider={<Divider />}
|
||||
orientation="vertical"
|
||||
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
||||
>
|
||||
{(
|
||||
[
|
||||
{
|
||||
key: 'Customer ID',
|
||||
value: (
|
||||
<Chip
|
||||
label={model.id}
|
||||
size="small"
|
||||
variant="soft"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ 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 => (
|
||||
<PropertyItem
|
||||
key={item.key}
|
||||
name={item.key}
|
||||
value={item.value}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
'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 { CrCategory } from '@/components/dashboard/cr/categories/type';
|
||||
|
||||
function getImageUrlFrRecord(record: CrCategory): string {
|
||||
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
||||
}
|
||||
|
||||
export default function SampleTitleCard({ lpModel }: { lpModel: CrCategory }): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{ alignItems: 'center', flex: '1 1 auto' }}
|
||||
>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
src={getImageUrlFrRecord(lpModel)}
|
||||
sx={{ '--Avatar-size': '64px' }}
|
||||
>
|
||||
{t('empty')}
|
||||
</Avatar>
|
||||
<div>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
|
||||
>
|
||||
<Typography variant="h4">{lpModel.cat_name}</Typography>
|
||||
<Chip
|
||||
icon={
|
||||
<CheckCircleIcon
|
||||
color="var(--mui-palette-success-main)"
|
||||
weight="fill"
|
||||
/>
|
||||
}
|
||||
label={lpModel.visible}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Stack>
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
variant="body1"
|
||||
>
|
||||
{lpModel.slug}
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<div>
|
||||
<Button
|
||||
endIcon={<CaretDownIcon />}
|
||||
variant="contained"
|
||||
>
|
||||
{t('list.action')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
138
002_source/cms/src/app/dashboard/cr/categories/[cat_id]/page.tsx
Normal file
138
002_source/cms/src/app/dashboard/cr/categories/[cat_id]/page.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
'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_CR_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 { defaultCrCategory } from '@/components/dashboard/cr/categories/_constants.ts';
|
||||
import { Notifications } from '@/components/dashboard/cr/categories/notifications';
|
||||
import type { CrCategory } from '@/components/dashboard/cr/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<boolean>(true);
|
||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||
|
||||
//
|
||||
const [showLessonCategory, setShowLessonCategory] = React.useState<CrCategory>(defaultCrCategory);
|
||||
|
||||
function handleEditClick() {
|
||||
router.push(paths.dashboard.cr_categories.edit(showLessonCategory.id));
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (catId) {
|
||||
pb.collection(COL_QUIZ_CR_CATEGORIES)
|
||||
.getOne(catId)
|
||||
.then((model: RecordModel) => {
|
||||
setShowLessonCategory({ ...defaultCrCategory, ...model });
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(err);
|
||||
toast(t('list.error'));
|
||||
|
||||
setShowError({ show: true, detail: JSON.stringify(err) });
|
||||
})
|
||||
.finally(() => {
|
||||
setShowLoading(false);
|
||||
});
|
||||
}
|
||||
}, [catId]);
|
||||
|
||||
if (showLoading) return <FormLoading />;
|
||||
if (showError.show)
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request')}
|
||||
code="500"
|
||||
details={showError.detail}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack spacing={3}>
|
||||
<div>
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.cr_categories.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
{t('list.title')}
|
||||
</Link>
|
||||
</div>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={3}
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
>
|
||||
<TitleCard lpModel={showLessonCategory} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
>
|
||||
<Grid
|
||||
lg={4}
|
||||
xs={12}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<BasicDetailCard
|
||||
lpModel={showLessonCategory}
|
||||
handleEditClick={handleEditClick}
|
||||
/>
|
||||
<SampleSecurityCard />
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid
|
||||
lg={8}
|
||||
xs={12}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<SamplePaymentCard />
|
||||
<SampleAddressCard />
|
||||
<Notifications notifications={SampleNotifications} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
'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 { CrCategoryCreateForm } from '@/components/dashboard/cr/categories/cr-category-create-form';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
// RULES: follow the name of page directory e.g. cr/categoies -> cr_categories
|
||||
const { t } = useTranslation(['lp_categories']);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack spacing={3}>
|
||||
<div>
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.cr_categories.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
{t('title')}
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h4">{t('create.title')}</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<CrCategoryCreateForm />
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
# task
|
||||
|
||||
## instruction
|
||||
|
||||
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/_helloworld/page.tsx`
|
||||
|
||||
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_types/edit/[typeId]/page.tsx`
|
||||
|
||||
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_categories/edit/page.tsx`
|
||||
|
||||
please draft a tsx for showing error to user thanks,
|
@@ -0,0 +1,53 @@
|
||||
'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 { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
const { t } = useTranslation(['lp_categories']);
|
||||
|
||||
React.useEffect(() => {
|
||||
// console.log('helloworld');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack spacing={3}>
|
||||
<div>
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.cr_categories.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
{t('edit.title')}
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h4">{t('edit.title')}</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<CrCategoryEditForm />
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
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[];
|
275
002_source/cms/src/app/dashboard/cr/categories/page.tsx
Normal file
275
002_source/cms/src/app/dashboard/cr/categories/page.tsx
Normal file
@@ -0,0 +1,275 @@
|
||||
'use client';
|
||||
|
||||
// RULES:
|
||||
// contains list page for cr_categories (QuizCRCategories)
|
||||
// contain definition to collection only
|
||||
//
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { COL_QUIZ_CR_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 { defaultCrCategory } from '@/components/dashboard/cr/categories/_constants';
|
||||
import { CrCategoriesFilters } from '@/components/dashboard/cr/categories/cr-categories-filters';
|
||||
import type { Filters } from '@/components/dashboard/cr/categories/cr-categories-filters';
|
||||
import { CrCategoriesPagination } from '@/components/dashboard/cr/categories/cr-categories-pagination';
|
||||
import { CrCategoriesSelectionProvider } from '@/components/dashboard/cr/categories/cr-categories-selection-context';
|
||||
import { CrCategoriesTable } from '@/components/dashboard/cr/categories/cr-categories-table';
|
||||
import type { CrCategory } from '@/components/dashboard/cr/categories/type';
|
||||
import ErrorDisplay from '@/components/dashboard/error';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const { t } = useTranslation(['lp_categories']);
|
||||
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||
const router = useRouter();
|
||||
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<CrCategory[]>([]);
|
||||
//
|
||||
|
||||
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
||||
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||
//
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||
//
|
||||
const [f, setF] = React.useState<CrCategory[]>([]);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||
//
|
||||
const [recordCount, setRecordCount] = React.useState<number>(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<void> => {
|
||||
try {
|
||||
const models: ListResult<RecordModel> = await pb
|
||||
.collection(COL_QUIZ_CR_CATEGORIES)
|
||||
.getList(currentPage + 1, rowsPerPage, listOption);
|
||||
const { items, totalItems } = models;
|
||||
const tempLessonTypes: CrCategory[] = items.map((lt) => {
|
||||
return { ...defaultCrCategory, ...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 <FormLoading />;
|
||||
|
||||
if (showError.show)
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request')}
|
||||
code={-1}
|
||||
details={showError.detail}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={3}
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
>
|
||||
<Box sx={{ flex: '1 1 auto' }}>
|
||||
<Typography variant="h4">{t('list.title')}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<LoadingButton
|
||||
loading={isLoadingAddPage}
|
||||
onClick={(): void => {
|
||||
setIsLoadingAddPage(true);
|
||||
router.push(paths.dashboard.cr_categories.create);
|
||||
}}
|
||||
startIcon={<PlusIcon />}
|
||||
variant="contained"
|
||||
>
|
||||
{t('list.add')}
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<CrCategoriesSelectionProvider lessonCategories={f}>
|
||||
<Card>
|
||||
<CrCategoriesFilters
|
||||
filters={{ email, phone, status, name, visible, type }}
|
||||
fullData={lessonCategoriesData}
|
||||
sortDir={sortDir}
|
||||
/>
|
||||
<Divider />
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<CrCategoriesTable
|
||||
reloadRows={reloadRows}
|
||||
rows={f}
|
||||
/>
|
||||
</Box>
|
||||
<Divider />
|
||||
<CrCategoriesPagination
|
||||
count={recordCount}
|
||||
page={currentPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
setPage={setCurrentPage}
|
||||
setRowsPerPage={setRowsPerPage}
|
||||
/>
|
||||
</Card>
|
||||
</CrCategoriesSelectionProvider>
|
||||
</Stack>
|
||||
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
|
||||
<pre>{JSON.stringify(f, null, 2)}</pre>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Sorting and filtering has to be done on the server.
|
||||
|
||||
function applySort(row: CrCategory[], sortDir: 'asc' | 'desc' | undefined): CrCategory[] {
|
||||
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: CrCategory[], { email, phone, status, name, visible }: Filters): CrCategory[] {
|
||||
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;
|
||||
//
|
||||
};
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
'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 { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||
|
||||
export default function BasicDetailCard({
|
||||
lpModel: model,
|
||||
handleEditClick,
|
||||
}: {
|
||||
lpModel: LpCategory;
|
||||
handleEditClick: () => void;
|
||||
}): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleEditClick();
|
||||
}}
|
||||
>
|
||||
<PencilSimpleIcon />
|
||||
</IconButton>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title={t('list.basic-details')}
|
||||
/>
|
||||
<PropertyList
|
||||
divider={<Divider />}
|
||||
orientation="vertical"
|
||||
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
||||
>
|
||||
{(
|
||||
[
|
||||
{
|
||||
key: 'Customer ID',
|
||||
value: (
|
||||
<Chip
|
||||
label={model.id}
|
||||
size="small"
|
||||
variant="soft"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ 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 => (
|
||||
<PropertyItem
|
||||
key={item.key}
|
||||
name={item.key}
|
||||
value={item.value}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
'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 { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||
|
||||
function getImageUrlFrRecord(record: LpCategory): string {
|
||||
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
||||
}
|
||||
|
||||
export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{ alignItems: 'center', flex: '1 1 auto' }}
|
||||
>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
src={getImageUrlFrRecord(lpModel)}
|
||||
sx={{ '--Avatar-size': '64px' }}
|
||||
>
|
||||
{t('empty')}
|
||||
</Avatar>
|
||||
<div>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
|
||||
>
|
||||
<Typography variant="h4">{lpModel.cat_name}</Typography>
|
||||
<Chip
|
||||
icon={
|
||||
<CheckCircleIcon
|
||||
color="var(--mui-palette-success-main)"
|
||||
weight="fill"
|
||||
/>
|
||||
}
|
||||
label={lpModel.visible}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Stack>
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
variant="body1"
|
||||
>
|
||||
{lpModel.slug}
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<div>
|
||||
<Button
|
||||
endIcon={<CaretDownIcon />}
|
||||
variant="contained"
|
||||
>
|
||||
{t('list.action')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
138
002_source/cms/src/app/dashboard/cr/questions/[cat_id]/page.tsx
Normal file
138
002_source/cms/src/app/dashboard/cr/questions/[cat_id]/page.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
'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_CR_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 { defaultCrQuestion } from '@/components/dashboard/cr/questions/_constants.ts';
|
||||
import { Notifications } from '@/components/dashboard/cr/questions/notifications';
|
||||
import type { CrQuestion } from '@/components/dashboard/cr/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<boolean>(true);
|
||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||
|
||||
//
|
||||
const [showLessonQuestion, setShowLessonQuestion] = React.useState<CrQuestion>(defaultCrQuestion);
|
||||
|
||||
function handleEditClick() {
|
||||
router.push(paths.dashboard.cr_questions.edit(showLessonQuestion.id));
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (catId) {
|
||||
pb.collection(COL_QUIZ_CR_QUESTIONS)
|
||||
.getOne(catId)
|
||||
.then((model: RecordModel) => {
|
||||
setShowLessonQuestion({ ...defaultCrQuestion, ...model });
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(err);
|
||||
toast(t('list.error'));
|
||||
|
||||
setShowError({ show: true, detail: JSON.stringify(err) });
|
||||
})
|
||||
.finally(() => {
|
||||
setShowLoading(false);
|
||||
});
|
||||
}
|
||||
}, [catId]);
|
||||
|
||||
if (showLoading) return <FormLoading />;
|
||||
if (showError.show)
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request')}
|
||||
code="500"
|
||||
details={showError.detail}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack spacing={3}>
|
||||
<div>
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.cr_questions.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
{t('edit.title')}
|
||||
</Link>
|
||||
</div>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={3}
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
>
|
||||
<TitleCard lpModel={showLessonQuestion} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
>
|
||||
<Grid
|
||||
lg={4}
|
||||
xs={12}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<BasicDetailCard
|
||||
lpModel={showLessonQuestion}
|
||||
handleEditClick={handleEditClick}
|
||||
/>
|
||||
<SampleSecurityCard />
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid
|
||||
lg={8}
|
||||
xs={12}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<SamplePaymentCard />
|
||||
<SampleAddressCard />
|
||||
<Notifications notifications={SampleNotifications} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||
|
||||
export const CrCategoriesSampleData = [
|
||||
{
|
||||
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[];
|
@@ -1,19 +1,24 @@
|
||||
'use client';
|
||||
|
||||
// RULES:
|
||||
// T.B.A.
|
||||
//
|
||||
import * as React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
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 { config } from '@/config';
|
||||
import { paths } from '@/paths';
|
||||
import { CustomerCreateForm } from '@/components/dashboard/customer/customer-create-form';
|
||||
|
||||
export const metadata = { title: `Create | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
import { CrQuestionCreateForm } from '@/components/dashboard/cr/questions/cr-question-create-form';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
// RULES: follow the name of page directory
|
||||
const { t } = useTranslation(['lp_questions']);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -29,19 +34,19 @@ export default function Page(): React.JSX.Element {
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.customers.list}
|
||||
href={paths.dashboard.lp_questions.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
Customers
|
||||
{t('title')}
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h4">Create customer</Typography>
|
||||
<Typography variant="h4">{t('create.title')}</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<CustomerCreateForm />
|
||||
<CrQuestionCreateForm />
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
@@ -0,0 +1,11 @@
|
||||
# task
|
||||
|
||||
## instruction
|
||||
|
||||
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/_helloworld/page.tsx`
|
||||
|
||||
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_types/edit/[typeId]/page.tsx`
|
||||
|
||||
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_categories/edit/page.tsx`
|
||||
|
||||
please draft a tsx for showing error to user thanks,
|
@@ -1,19 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
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 { config } from '@/config';
|
||||
import { paths } from '@/paths';
|
||||
import { CustomerCreateForm } from '@/components/dashboard/customer/customer-create-form';
|
||||
|
||||
export const metadata = { title: `Create | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
import { CrQuestionEditForm } from '@/components/dashboard/cr/questions/cr-question-edit-form';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
const { t } = useTranslation(['lp_questions']);
|
||||
|
||||
React.useEffect(() => {
|
||||
// console.log('helloworld');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -29,19 +34,19 @@ export default function Page(): React.JSX.Element {
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.customers.list}
|
||||
href={paths.dashboard.lp_questions.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
Customers
|
||||
{t('edit.title')}
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h4">Create customer</Typography>
|
||||
<Typography variant="h4">{t('edit.title')}</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<CustomerCreateForm />
|
||||
<CrQuestionEditForm />
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
272
002_source/cms/src/app/dashboard/cr/questions/page.tsx
Normal file
272
002_source/cms/src/app/dashboard/cr/questions/page.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
'use client';
|
||||
|
||||
// RULES:
|
||||
// contains list page for cr_questions (QuizCRQuestions)
|
||||
// contain definition to collection only
|
||||
//
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { COL_QUIZ_CR_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 { defaultCrQuestion } from '@/components/dashboard/cr/questions/_constants';
|
||||
import { CrQuestionsFilters } from '@/components/dashboard/cr/questions/cr-questions-filters';
|
||||
import type { Filters } from '@/components/dashboard/cr/questions/cr-questions-filters';
|
||||
import { CrQuestionsPagination } from '@/components/dashboard/cr/questions/cr-questions-pagination';
|
||||
import { CrQuestionsSelectionProvider } from '@/components/dashboard/cr/questions/cr-questions-selection-context';
|
||||
import { CrQuestionsTable } from '@/components/dashboard/cr/questions/cr-questions-table';
|
||||
import type { CrQuestion } from '@/components/dashboard/cr/questions/type';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const { t } = useTranslation(['lp_questions']);
|
||||
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||
const router = useRouter();
|
||||
const [lessonQuestionsData, setLessonCategoriesData] = React.useState<CrQuestion[]>([]);
|
||||
//
|
||||
|
||||
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
||||
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||
//
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||
//
|
||||
const [f, setF] = React.useState<CrQuestion[]>([]);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||
//
|
||||
const [recordCount, setRecordCount] = React.useState<number>(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<void> => {
|
||||
try {
|
||||
const models: ListResult<RecordModel> = await pb
|
||||
.collection(COL_QUIZ_CR_QUESTIONS)
|
||||
.getList(currentPage + 1, rowsPerPage, listOption);
|
||||
const { items, totalItems } = models;
|
||||
const tempLessonTypes: CrQuestion[] = items.map((lt) => {
|
||||
return { ...defaultCrQuestion, ...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 <FormLoading />;
|
||||
|
||||
if (showError.show)
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request')}
|
||||
code={-1}
|
||||
details={showError.detail}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={3}
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
>
|
||||
<Box sx={{ flex: '1 1 auto' }}>
|
||||
<Typography variant="h4">{t('list.title')}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<LoadingButton
|
||||
loading={isLoadingAddPage}
|
||||
onClick={(): void => {
|
||||
setIsLoadingAddPage(true);
|
||||
router.push(paths.dashboard.cr_questions.create);
|
||||
}}
|
||||
startIcon={<PlusIcon />}
|
||||
variant="contained"
|
||||
>
|
||||
{t('list.add')}
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<CrQuestionsSelectionProvider lessonQuestions={f}>
|
||||
<Card>
|
||||
<CrQuestionsFilters
|
||||
filters={{ email, phone, status, name, visible, type }}
|
||||
fullData={lessonQuestionsData}
|
||||
sortDir={sortDir}
|
||||
/>
|
||||
<Divider />
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<CrQuestionsTable
|
||||
reloadRows={reloadRows}
|
||||
rows={f}
|
||||
/>
|
||||
</Box>
|
||||
<Divider />
|
||||
<CrQuestionsPagination
|
||||
count={recordCount}
|
||||
page={currentPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
setPage={setCurrentPage}
|
||||
setRowsPerPage={setRowsPerPage}
|
||||
/>
|
||||
</Card>
|
||||
</CrQuestionsSelectionProvider>
|
||||
</Stack>
|
||||
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
|
||||
<pre>{JSON.stringify(f, null, 2)}</pre>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Sorting and filtering has to be done on the server.
|
||||
|
||||
function applySort(row: CrQuestion[], sortDir: 'asc' | 'desc' | undefined): CrQuestion[] {
|
||||
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: CrQuestion[], { email, phone, status, name, visible }: Filters): CrQuestion[] {
|
||||
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;
|
||||
//
|
||||
};
|
||||
}
|
@@ -1,308 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import RouterLink from 'next/link';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||
import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard';
|
||||
import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House';
|
||||
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
||||
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { paths } from '@/paths';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { PropertyItem } from '@/components/core/property-item';
|
||||
import { PropertyList } from '@/components/core/property-list';
|
||||
import { Notifications } from '@/components/dashboard/customer/notifications';
|
||||
import { Payments } from '@/components/dashboard/customer/payments';
|
||||
import type { Address } from '@/components/dashboard/customer/shipping-address';
|
||||
import { ShippingAddress } from '@/components/dashboard/customer/shipping-address';
|
||||
|
||||
export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack spacing={3}>
|
||||
<div>
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.customers.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
Customers
|
||||
</Link>
|
||||
</div>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
|
||||
<Avatar src="/assets/avatar-1.png" sx={{ '--Avatar-size': '64px' }}>
|
||||
MV
|
||||
</Avatar>
|
||||
<div>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Typography variant="h4">Miron Vitold</Typography>
|
||||
<Chip
|
||||
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
|
||||
label="Active"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Stack>
|
||||
<Typography color="text.secondary" variant="body1">
|
||||
miron.vitold@domain.com
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<div>
|
||||
<Button endIcon={<CaretDownIcon />} variant="contained">
|
||||
Action
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Grid container spacing={4}>
|
||||
<Grid lg={4} xs={12}>
|
||||
<Stack spacing={4}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton>
|
||||
<PencilSimpleIcon />
|
||||
</IconButton>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Basic details"
|
||||
/>
|
||||
<PropertyList
|
||||
divider={<Divider />}
|
||||
orientation="vertical"
|
||||
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
||||
>
|
||||
{(
|
||||
[
|
||||
{ key: 'Customer ID', value: <Chip label="USR-001" size="small" variant="soft" /> },
|
||||
{ key: 'Name', value: 'Miron Vitold' },
|
||||
{ key: 'Email', value: 'miron.vitold@domain.com' },
|
||||
{ key: 'Phone', value: '(425) 434-5535' },
|
||||
{ key: 'Company', value: 'Devias IO' },
|
||||
{
|
||||
key: 'Quota',
|
||||
value: (
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" />
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
50%
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Security"
|
||||
/>
|
||||
<CardContent>
|
||||
<Stack spacing={1}>
|
||||
<div>
|
||||
<Button color="error" variant="contained">
|
||||
Delete account
|
||||
</Button>
|
||||
</div>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
A deleted customer cannot be restored. All data will be permanently removed.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid lg={8} xs={12}>
|
||||
<Stack spacing={4}>
|
||||
<Payments
|
||||
ordersValue={2069.48}
|
||||
payments={[
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 500,
|
||||
invoiceId: 'INV-005',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(5, 'minute').subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 324.5,
|
||||
invoiceId: 'INV-004',
|
||||
status: 'refunded',
|
||||
createdAt: dayjs().subtract(21, 'minute').subtract(2, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 746.5,
|
||||
invoiceId: 'INV-003',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(7, 'minute').subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 56.89,
|
||||
invoiceId: 'INV-002',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(48, 'minute').subtract(4, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 541.59,
|
||||
invoiceId: 'INV-001',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(31, 'minute').subtract(5, 'hour').toDate(),
|
||||
},
|
||||
]}
|
||||
refundsValue={324.5}
|
||||
totalOrders={5}
|
||||
/>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
|
||||
Edit
|
||||
</Button>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Billing details"
|
||||
/>
|
||||
<CardContent>
|
||||
<Card sx={{ borderRadius: 1 }} variant="outlined">
|
||||
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}>
|
||||
{(
|
||||
[
|
||||
{ key: 'Credit card', value: '**** 4142' },
|
||||
{ key: 'Country', value: 'United States' },
|
||||
{ key: 'State', value: 'Michigan' },
|
||||
{ key: 'City', value: 'Southfield' },
|
||||
{ key: 'Address', value: '1721 Bartlett Avenue, 48034' },
|
||||
{ key: 'Tax ID', value: 'EU87956621' },
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button color="secondary" startIcon={<PlusIcon />}>
|
||||
Add
|
||||
</Button>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<HouseIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Shipping addresses"
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid container spacing={3}>
|
||||
{(
|
||||
[
|
||||
{
|
||||
id: 'ADR-001',
|
||||
country: 'United States',
|
||||
state: 'Michigan',
|
||||
city: 'Lansing',
|
||||
zipCode: '48933',
|
||||
street: '480 Haven Lane',
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
id: 'ADR-002',
|
||||
country: 'United States',
|
||||
state: 'Missouri',
|
||||
city: 'Springfield',
|
||||
zipCode: '65804',
|
||||
street: '4807 Lighthouse Drive',
|
||||
},
|
||||
] satisfies Address[]
|
||||
).map((address) => (
|
||||
<Grid key={address.id} md={6} xs={12}>
|
||||
<ShippingAddress address={address} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Notifications
|
||||
notifications={[
|
||||
{
|
||||
id: 'EV-002',
|
||||
type: 'Refund request approved',
|
||||
status: 'pending',
|
||||
createdAt: dayjs().subtract(34, 'minute').subtract(5, 'hour').subtract(3, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'EV-001',
|
||||
type: 'Order confirmation',
|
||||
status: 'delivered',
|
||||
createdAt: dayjs().subtract(49, 'minute').subtract(11, 'hour').subtract(4, 'day').toDate(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -1,255 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { CustomersFilters } from '@/components/dashboard/customer/customers-filters';
|
||||
import type { Filters } from '@/components/dashboard/customer/customers-filters';
|
||||
import { CustomersPagination } from '@/components/dashboard/customer/customers-pagination';
|
||||
import { CustomersSelectionProvider } from '@/components/dashboard/customer/customers-selection-context';
|
||||
import { CustomersTable } from '@/components/dashboard/customer/customers-table';
|
||||
import type { Customer } from '@/components/dashboard/customer/customers-table';
|
||||
|
||||
export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
|
||||
const customers = [
|
||||
{
|
||||
id: 'USR-005',
|
||||
name: 'Fran Perez',
|
||||
avatar: '/assets/avatar-5.png',
|
||||
email: 'fran.perez@domain.com',
|
||||
phone: '(815) 704-0045',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-004',
|
||||
name: 'Penjani Inyene',
|
||||
avatar: '/assets/avatar-4.png',
|
||||
email: 'penjani.inyene@domain.com',
|
||||
phone: '(803) 937-8925',
|
||||
quota: 100,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-003',
|
||||
name: 'Carson Darrin',
|
||||
avatar: '/assets/avatar-3.png',
|
||||
email: 'carson.darrin@domain.com',
|
||||
phone: '(715) 278-5041',
|
||||
quota: 10,
|
||||
status: 'blocked',
|
||||
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-002',
|
||||
name: 'Siegbert Gottfried',
|
||||
avatar: '/assets/avatar-2.png',
|
||||
email: 'siegbert.gottfried@domain.com',
|
||||
phone: '(603) 766-0431',
|
||||
quota: 0,
|
||||
status: 'pending',
|
||||
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-001',
|
||||
name: 'Miron Vitold',
|
||||
avatar: '/assets/avatar-1.png',
|
||||
email: 'miron.vitold@domain.com',
|
||||
phone: '(425) 434-5535',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-005',
|
||||
name: 'Fran Perez',
|
||||
avatar: '/assets/avatar-5.png',
|
||||
email: 'fran.perez@domain.com',
|
||||
phone: '(815) 704-0045',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-004',
|
||||
name: 'Penjani Inyene',
|
||||
avatar: '/assets/avatar-4.png',
|
||||
email: 'penjani.inyene@domain.com',
|
||||
phone: '(803) 937-8925',
|
||||
quota: 100,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-003',
|
||||
name: 'Carson Darrin',
|
||||
avatar: '/assets/avatar-3.png',
|
||||
email: 'carson.darrin@domain.com',
|
||||
phone: '(715) 278-5041',
|
||||
quota: 10,
|
||||
status: 'blocked',
|
||||
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-002',
|
||||
name: 'Siegbert Gottfried',
|
||||
avatar: '/assets/avatar-2.png',
|
||||
email: 'siegbert.gottfried@domain.com',
|
||||
phone: '(603) 766-0431',
|
||||
quota: 0,
|
||||
status: 'pending',
|
||||
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-001',
|
||||
name: 'Miron Vitold',
|
||||
avatar: '/assets/avatar-1.png',
|
||||
email: 'miron.vitold@domain.com',
|
||||
phone: '(425) 434-5535',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-005',
|
||||
name: 'Fran Perez',
|
||||
avatar: '/assets/avatar-5.png',
|
||||
email: 'fran.perez@domain.com',
|
||||
phone: '(815) 704-0045',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-004',
|
||||
name: 'Penjani Inyene',
|
||||
avatar: '/assets/avatar-4.png',
|
||||
email: 'penjani.inyene@domain.com',
|
||||
phone: '(803) 937-8925',
|
||||
quota: 100,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-003',
|
||||
name: 'Carson Darrin',
|
||||
avatar: '/assets/avatar-3.png',
|
||||
email: 'carson.darrin@domain.com',
|
||||
phone: '(715) 278-5041',
|
||||
quota: 10,
|
||||
status: 'blocked',
|
||||
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-002',
|
||||
name: 'Siegbert Gottfried',
|
||||
avatar: '/assets/avatar-2.png',
|
||||
email: 'siegbert.gottfried@domain.com',
|
||||
phone: '(603) 766-0431',
|
||||
quota: 0,
|
||||
status: 'pending',
|
||||
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-001',
|
||||
name: 'Miron Vitold',
|
||||
avatar: '/assets/avatar-1.png',
|
||||
email: 'miron.vitold@domain.com',
|
||||
phone: '(425) 434-5535',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
||||
},
|
||||
] satisfies Customer[];
|
||||
|
||||
interface PageProps {
|
||||
searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string };
|
||||
}
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const { email, phone, sortDir, status } = searchParams;
|
||||
|
||||
const sortedCustomers = applySort(customers, sortDir);
|
||||
const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status });
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||
<Box sx={{ flex: '1 1 auto' }}>
|
||||
<Typography variant="h4">Customers</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button startIcon={<PlusIcon />} variant="contained">
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
<CustomersSelectionProvider customers={filteredCustomers}>
|
||||
<Card>
|
||||
<CustomersFilters filters={{ email, phone, status }} sortDir={sortDir} />
|
||||
<Divider />
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<CustomersTable rows={filteredCustomers} />
|
||||
</Box>
|
||||
<Divider />
|
||||
<CustomersPagination count={filteredCustomers.length + 100} page={0} />
|
||||
</Card>
|
||||
</CustomersSelectionProvider>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Sorting and filtering has to be done on the server.
|
||||
|
||||
function applySort(row: Customer[], sortDir: 'asc' | 'desc' | undefined): Customer[] {
|
||||
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: Customer[], { email, phone, status }: Filters): Customer[] {
|
||||
return row.filter((item) => {
|
||||
if (email) {
|
||||
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (phone) {
|
||||
if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (status) {
|
||||
if (item.status !== status) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
@@ -1,308 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import RouterLink from 'next/link';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||
import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard';
|
||||
import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House';
|
||||
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
||||
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { paths } from '@/paths';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { PropertyItem } from '@/components/core/property-item';
|
||||
import { PropertyList } from '@/components/core/property-list';
|
||||
import { Notifications } from '@/components/dashboard/customer/notifications';
|
||||
import { Payments } from '@/components/dashboard/customer/payments';
|
||||
import type { Address } from '@/components/dashboard/customer/shipping-address';
|
||||
import { ShippingAddress } from '@/components/dashboard/customer/shipping-address';
|
||||
|
||||
export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack spacing={3}>
|
||||
<div>
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.customers.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
Customers
|
||||
</Link>
|
||||
</div>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
|
||||
<Avatar src="/assets/avatar-1.png" sx={{ '--Avatar-size': '64px' }}>
|
||||
MV
|
||||
</Avatar>
|
||||
<div>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Typography variant="h4">Miron Vitold</Typography>
|
||||
<Chip
|
||||
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
|
||||
label="Active"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Stack>
|
||||
<Typography color="text.secondary" variant="body1">
|
||||
miron.vitold@domain.com
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<div>
|
||||
<Button endIcon={<CaretDownIcon />} variant="contained">
|
||||
Action
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Grid container spacing={4}>
|
||||
<Grid lg={4} xs={12}>
|
||||
<Stack spacing={4}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton>
|
||||
<PencilSimpleIcon />
|
||||
</IconButton>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Basic details"
|
||||
/>
|
||||
<PropertyList
|
||||
divider={<Divider />}
|
||||
orientation="vertical"
|
||||
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
||||
>
|
||||
{(
|
||||
[
|
||||
{ key: 'Customer ID', value: <Chip label="USR-001" size="small" variant="soft" /> },
|
||||
{ key: 'Name', value: 'Miron Vitold' },
|
||||
{ key: 'Email', value: 'miron.vitold@domain.com' },
|
||||
{ key: 'Phone', value: '(425) 434-5535' },
|
||||
{ key: 'Company', value: 'Devias IO' },
|
||||
{
|
||||
key: 'Quota',
|
||||
value: (
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" />
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
50%
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Security"
|
||||
/>
|
||||
<CardContent>
|
||||
<Stack spacing={1}>
|
||||
<div>
|
||||
<Button color="error" variant="contained">
|
||||
Delete account
|
||||
</Button>
|
||||
</div>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
A deleted customer cannot be restored. All data will be permanently removed.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid lg={8} xs={12}>
|
||||
<Stack spacing={4}>
|
||||
<Payments
|
||||
ordersValue={2069.48}
|
||||
payments={[
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 500,
|
||||
invoiceId: 'INV-005',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(5, 'minute').subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 324.5,
|
||||
invoiceId: 'INV-004',
|
||||
status: 'refunded',
|
||||
createdAt: dayjs().subtract(21, 'minute').subtract(2, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 746.5,
|
||||
invoiceId: 'INV-003',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(7, 'minute').subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 56.89,
|
||||
invoiceId: 'INV-002',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(48, 'minute').subtract(4, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 541.59,
|
||||
invoiceId: 'INV-001',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(31, 'minute').subtract(5, 'hour').toDate(),
|
||||
},
|
||||
]}
|
||||
refundsValue={324.5}
|
||||
totalOrders={5}
|
||||
/>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
|
||||
Edit
|
||||
</Button>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Billing details"
|
||||
/>
|
||||
<CardContent>
|
||||
<Card sx={{ borderRadius: 1 }} variant="outlined">
|
||||
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}>
|
||||
{(
|
||||
[
|
||||
{ key: 'Credit card', value: '**** 4142' },
|
||||
{ key: 'Country', value: 'United States' },
|
||||
{ key: 'State', value: 'Michigan' },
|
||||
{ key: 'City', value: 'Southfield' },
|
||||
{ key: 'Address', value: '1721 Bartlett Avenue, 48034' },
|
||||
{ key: 'Tax ID', value: 'EU87956621' },
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button color="secondary" startIcon={<PlusIcon />}>
|
||||
Add
|
||||
</Button>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<HouseIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Shipping addresses"
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid container spacing={3}>
|
||||
{(
|
||||
[
|
||||
{
|
||||
id: 'ADR-001',
|
||||
country: 'United States',
|
||||
state: 'Michigan',
|
||||
city: 'Lansing',
|
||||
zipCode: '48933',
|
||||
street: '480 Haven Lane',
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
id: 'ADR-002',
|
||||
country: 'United States',
|
||||
state: 'Missouri',
|
||||
city: 'Springfield',
|
||||
zipCode: '65804',
|
||||
street: '4807 Lighthouse Drive',
|
||||
},
|
||||
] satisfies Address[]
|
||||
).map((address) => (
|
||||
<Grid key={address.id} md={6} xs={12}>
|
||||
<ShippingAddress address={address} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Notifications
|
||||
notifications={[
|
||||
{
|
||||
id: 'EV-002',
|
||||
type: 'Refund request approved',
|
||||
status: 'pending',
|
||||
createdAt: dayjs().subtract(34, 'minute').subtract(5, 'hour').subtract(3, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'EV-001',
|
||||
type: 'Order confirmation',
|
||||
status: 'delivered',
|
||||
createdAt: dayjs().subtract(49, 'minute').subtract(11, 'hour').subtract(4, 'day').toDate(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -1,255 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { CustomersFilters } from '@/components/dashboard/customer/customers-filters';
|
||||
import type { Filters } from '@/components/dashboard/customer/customers-filters';
|
||||
import { CustomersPagination } from '@/components/dashboard/customer/customers-pagination';
|
||||
import { CustomersSelectionProvider } from '@/components/dashboard/customer/customers-selection-context';
|
||||
import { CustomersTable } from '@/components/dashboard/customer/customers-table';
|
||||
import type { Customer } from '@/components/dashboard/customer/customers-table';
|
||||
|
||||
export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
|
||||
const customers = [
|
||||
{
|
||||
id: 'USR-005',
|
||||
name: 'Fran Perez',
|
||||
avatar: '/assets/avatar-5.png',
|
||||
email: 'fran.perez@domain.com',
|
||||
phone: '(815) 704-0045',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-004',
|
||||
name: 'Penjani Inyene',
|
||||
avatar: '/assets/avatar-4.png',
|
||||
email: 'penjani.inyene@domain.com',
|
||||
phone: '(803) 937-8925',
|
||||
quota: 100,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-003',
|
||||
name: 'Carson Darrin',
|
||||
avatar: '/assets/avatar-3.png',
|
||||
email: 'carson.darrin@domain.com',
|
||||
phone: '(715) 278-5041',
|
||||
quota: 10,
|
||||
status: 'blocked',
|
||||
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-002',
|
||||
name: 'Siegbert Gottfried',
|
||||
avatar: '/assets/avatar-2.png',
|
||||
email: 'siegbert.gottfried@domain.com',
|
||||
phone: '(603) 766-0431',
|
||||
quota: 0,
|
||||
status: 'pending',
|
||||
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-001',
|
||||
name: 'Miron Vitold',
|
||||
avatar: '/assets/avatar-1.png',
|
||||
email: 'miron.vitold@domain.com',
|
||||
phone: '(425) 434-5535',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-005',
|
||||
name: 'Fran Perez',
|
||||
avatar: '/assets/avatar-5.png',
|
||||
email: 'fran.perez@domain.com',
|
||||
phone: '(815) 704-0045',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-004',
|
||||
name: 'Penjani Inyene',
|
||||
avatar: '/assets/avatar-4.png',
|
||||
email: 'penjani.inyene@domain.com',
|
||||
phone: '(803) 937-8925',
|
||||
quota: 100,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-003',
|
||||
name: 'Carson Darrin',
|
||||
avatar: '/assets/avatar-3.png',
|
||||
email: 'carson.darrin@domain.com',
|
||||
phone: '(715) 278-5041',
|
||||
quota: 10,
|
||||
status: 'blocked',
|
||||
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-002',
|
||||
name: 'Siegbert Gottfried',
|
||||
avatar: '/assets/avatar-2.png',
|
||||
email: 'siegbert.gottfried@domain.com',
|
||||
phone: '(603) 766-0431',
|
||||
quota: 0,
|
||||
status: 'pending',
|
||||
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-001',
|
||||
name: 'Miron Vitold',
|
||||
avatar: '/assets/avatar-1.png',
|
||||
email: 'miron.vitold@domain.com',
|
||||
phone: '(425) 434-5535',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-005',
|
||||
name: 'Fran Perez',
|
||||
avatar: '/assets/avatar-5.png',
|
||||
email: 'fran.perez@domain.com',
|
||||
phone: '(815) 704-0045',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-004',
|
||||
name: 'Penjani Inyene',
|
||||
avatar: '/assets/avatar-4.png',
|
||||
email: 'penjani.inyene@domain.com',
|
||||
phone: '(803) 937-8925',
|
||||
quota: 100,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-003',
|
||||
name: 'Carson Darrin',
|
||||
avatar: '/assets/avatar-3.png',
|
||||
email: 'carson.darrin@domain.com',
|
||||
phone: '(715) 278-5041',
|
||||
quota: 10,
|
||||
status: 'blocked',
|
||||
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-002',
|
||||
name: 'Siegbert Gottfried',
|
||||
avatar: '/assets/avatar-2.png',
|
||||
email: 'siegbert.gottfried@domain.com',
|
||||
phone: '(603) 766-0431',
|
||||
quota: 0,
|
||||
status: 'pending',
|
||||
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-001',
|
||||
name: 'Miron Vitold',
|
||||
avatar: '/assets/avatar-1.png',
|
||||
email: 'miron.vitold@domain.com',
|
||||
phone: '(425) 434-5535',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
||||
},
|
||||
] satisfies Customer[];
|
||||
|
||||
interface PageProps {
|
||||
searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string };
|
||||
}
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const { email, phone, sortDir, status } = searchParams;
|
||||
|
||||
const sortedCustomers = applySort(customers, sortDir);
|
||||
const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status });
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||
<Box sx={{ flex: '1 1 auto' }}>
|
||||
<Typography variant="h4">Customers</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button startIcon={<PlusIcon />} variant="contained">
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
<CustomersSelectionProvider customers={filteredCustomers}>
|
||||
<Card>
|
||||
<CustomersFilters filters={{ email, phone, status }} sortDir={sortDir} />
|
||||
<Divider />
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<CustomersTable rows={filteredCustomers} />
|
||||
</Box>
|
||||
<Divider />
|
||||
<CustomersPagination count={filteredCustomers.length + 100} page={0} />
|
||||
</Card>
|
||||
</CustomersSelectionProvider>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Sorting and filtering has to be done on the server.
|
||||
|
||||
function applySort(row: Customer[], sortDir: 'asc' | 'desc' | undefined): Customer[] {
|
||||
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: Customer[], { email, phone, status }: Filters): Customer[] {
|
||||
return row.filter((item) => {
|
||||
if (email) {
|
||||
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (phone) {
|
||||
if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (status) {
|
||||
if (item.status !== status) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
@@ -1,5 +1,9 @@
|
||||
'use client';
|
||||
|
||||
// RULES:
|
||||
// contains list page for lesson_categories (LessonCategories)
|
||||
// contain definition to collection only
|
||||
//
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { COL_LESSON_CATEGORIES } from '@/constants';
|
||||
@@ -14,6 +18,7 @@ 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';
|
||||
@@ -24,7 +29,7 @@ import type { Filters } from '@/components/dashboard/lesson_category/lesson-cate
|
||||
import { LessonCategoriesPagination } from '@/components/dashboard/lesson_category/lesson-categories-pagination';
|
||||
import { LessonCategoriesSelectionProvider } from '@/components/dashboard/lesson_category/lesson-categories-selection-context';
|
||||
import { LessonCategoriesTable } from '@/components/dashboard/lesson_category/lesson-categories-table';
|
||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
@@ -39,60 +44,138 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
|
||||
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
||||
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||
const [showError, setShowError] = React.useState<boolean>(false);
|
||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||
//
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||
//
|
||||
const [f, setF] = React.useState<LessonCategory[]>([]);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(1);
|
||||
//
|
||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||
|
||||
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
|
||||
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status: status });
|
||||
const [listOption, setListOption] = React.useState({});
|
||||
const [listSort, setListSort] = React.useState({});
|
||||
|
||||
//
|
||||
const reloadRows = () => {
|
||||
setShowLoading(true);
|
||||
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
|
||||
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status });
|
||||
|
||||
pb.collection(COL_LESSON_CATEGORIES)
|
||||
.getList(currentPage, rowsPerPage, {})
|
||||
.then((lessonCategories: ListResult<RecordModel>) => {
|
||||
// console.log(lessonTypes);
|
||||
const { items, page, perPage, totalItems, totalPages } = lessonCategories;
|
||||
const tempLessonCategories: LessonCategory[] = items.map((item) => {
|
||||
return { ...defaultLessonCategory, ...item };
|
||||
});
|
||||
|
||||
setLessonCategoriesData(tempLessonCategories);
|
||||
setRecordCount(totalItems);
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(err);
|
||||
toast(t('dashboard.lessonTypes.list.error'));
|
||||
setShowError(true);
|
||||
})
|
||||
.finally(() => {
|
||||
setShowLoading(false);
|
||||
//
|
||||
const reloadRows = async (): Promise<void> => {
|
||||
try {
|
||||
const models: ListResult<RecordModel> = await pb
|
||||
.collection(COL_LESSON_CATEGORIES)
|
||||
.getList(currentPage + 1, rowsPerPage, listOption);
|
||||
const { items, totalItems } = models;
|
||||
const tempLessonTypes: LessonCategory[] = items.map((lt) => {
|
||||
return { ...defaultLessonCategory, ...lt };
|
||||
});
|
||||
|
||||
setLessonCategoriesData(tempLessonTypes);
|
||||
setRecordCount(totalItems);
|
||||
setF(tempLessonTypes);
|
||||
// console.log({ currentPage, f });
|
||||
} catch (error) {
|
||||
//
|
||||
logger.error(error);
|
||||
setShowError({
|
||||
//
|
||||
show: true,
|
||||
detail: JSON.stringify(error, null, 2),
|
||||
});
|
||||
} finally {
|
||||
setShowLoading(false);
|
||||
}
|
||||
|
||||
// pb.collection(COL_LESSON_CATEGORIES)
|
||||
// .getList(currentPage, rowsPerPage, listOption)
|
||||
// .then((lessonCategories: ListResult<RecordModel>) => {
|
||||
// // console.log(lessonTypes);
|
||||
// const { items, page, perPage, totalItems, totalPages } = lessonCategories;
|
||||
// const tempLessonCategories: LessonCategory[] = items.map((item) => {
|
||||
// return { ...defaultLessonCategory, ...item };
|
||||
// });
|
||||
|
||||
// setLessonCategoriesData(tempLessonCategories);
|
||||
// setRecordCount(totalItems);
|
||||
// setF(tempLessonCategories);
|
||||
// // console.log({ currentPage, f });
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// logger.error(error);
|
||||
// setShowError({
|
||||
// //
|
||||
// show: true,
|
||||
// detail: JSON.stringify(error, null, 2),
|
||||
// });
|
||||
// })
|
||||
// .finally(() => {
|
||||
// setShowLoading(false);
|
||||
// });
|
||||
};
|
||||
|
||||
const [lastListOption, setLastListOption] = React.useState({});
|
||||
const isFirstRun = React.useRef(false);
|
||||
React.useEffect(() => {
|
||||
reloadRows();
|
||||
}, []);
|
||||
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 <FormLoading />;
|
||||
|
||||
if (showError)
|
||||
if (showError.show)
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request', { ns: 'common' })}
|
||||
message={t('error.unable-to-process-request')}
|
||||
code="500"
|
||||
details={t('error.detailed-error-information', { ns: 'common' })}
|
||||
details={showError.detail}
|
||||
/>
|
||||
);
|
||||
|
||||
// return <pre>{JSON.stringify(lessonCategoriesData, null, 2)}</pre>;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -103,7 +186,11 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={3}
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
>
|
||||
<Box sx={{ flex: '1 1 auto' }}>
|
||||
<Typography variant="h4">{t('list.title')}</Typography>
|
||||
</Box>
|
||||
@@ -117,6 +204,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
startIcon={<PlusIcon />}
|
||||
variant="contained"
|
||||
>
|
||||
{/* add new lesson type */}
|
||||
{t('list.add')}
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
@@ -130,13 +218,25 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
/>
|
||||
<Divider />
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<LessonCategoriesTable reloadRows={reloadRows} rows={filteredLessonCategories} />
|
||||
<LessonCategoriesTable
|
||||
reloadRows={reloadRows}
|
||||
rows={f}
|
||||
/>
|
||||
</Box>
|
||||
<Divider />
|
||||
<LessonCategoriesPagination count={filteredLessonCategories.length + 100} page={0} />
|
||||
<LessonCategoriesPagination
|
||||
count={recordCount}
|
||||
page={currentPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
setPage={setCurrentPage}
|
||||
setRowsPerPage={setRowsPerPage}
|
||||
/>
|
||||
</Card>
|
||||
</LessonCategoriesSelectionProvider>
|
||||
</Stack>
|
||||
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
|
||||
<pre>{JSON.stringify(f, null, 2)}</pre>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -153,19 +253,19 @@ function applySort(row: LessonCategory[], sortDir: 'asc' | 'desc' | undefined):
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters(row: LessonCategory[], { email, phone, status }: Filters): LessonCategory[] {
|
||||
function applyFilters(row: LessonCategory[], { email, phone, status, name, visible }: Filters): LessonCategory[] {
|
||||
return row.filter((item) => {
|
||||
// if (email) {
|
||||
// if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
if (email) {
|
||||
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// if (phone) {
|
||||
// if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
if (phone) {
|
||||
if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (status) {
|
||||
if (item.status !== status) {
|
||||
@@ -173,6 +273,18 @@ function applyFilters(row: LessonCategory[], { email, phone, status }: Filters):
|
||||
}
|
||||
}
|
||||
|
||||
if (name) {
|
||||
if (!item.name?.toLowerCase().includes(name.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -186,6 +298,5 @@ interface PageProps {
|
||||
name?: string;
|
||||
visible?: string;
|
||||
type?: string;
|
||||
//
|
||||
};
|
||||
}
|
||||
|
@@ -6,9 +6,9 @@ import { useParams, useRouter } from 'next/navigation';
|
||||
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
|
||||
import BasicDetailCard from '@/app/dashboard/Sample/BasicDetailCard';
|
||||
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
|
||||
import SamplePaymentCard from '@/app/dashboard/Sample/SamplePaymentCard';
|
||||
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
|
||||
import SampleTitleCard from '@/app/dashboard/Sample/SampleTitleCard';
|
||||
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
|
||||
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
|
||||
import SampleTitleCard from '@/app/dashboard/Sample/TitleCard';
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
// RULES:
|
||||
// contains list page for lp_categories (QuizLPCategories)
|
||||
// contains list page for lesson_types (LessonTypes)
|
||||
// contain definition to collection only
|
||||
//
|
||||
import * as React from 'react';
|
||||
@@ -18,6 +18,7 @@ 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';
|
||||
@@ -43,8 +44,10 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||
//
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||
//
|
||||
const [f, setF] = React.useState<LessonType[]>([]);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||
//
|
||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||
const [listOption, setListOption] = React.useState({});
|
||||
const [listSort, setListSort] = React.useState({});
|
||||
@@ -63,17 +66,33 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
setLessonTypesData(tempLessonTypes);
|
||||
setRecordCount(totalItems);
|
||||
setF(tempLessonTypes);
|
||||
// console.log({ currentPage, f });
|
||||
} catch (error) {
|
||||
//
|
||||
logger.error(error);
|
||||
setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
|
||||
setShowError({
|
||||
//
|
||||
show: true,
|
||||
detail: JSON.stringify(error, null, 2),
|
||||
});
|
||||
} finally {
|
||||
setShowLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const [lastListOption, setLastListOption] = React.useState({});
|
||||
const isFirstRun = React.useRef(false);
|
||||
React.useEffect(() => {
|
||||
void reloadRows();
|
||||
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(() => {
|
||||
@@ -96,16 +115,26 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
tempFilter.push(`type ~ "%${type}%"`);
|
||||
}
|
||||
|
||||
setListOption({
|
||||
filter: tempFilter.join(' && '),
|
||||
sort: tempSortDir,
|
||||
//
|
||||
});
|
||||
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]);
|
||||
|
||||
if (f.length === 0 || showLoading) return <FormLoading />;
|
||||
// return <>helloworld</>;
|
||||
|
||||
if (showError)
|
||||
if (showLoading) return <FormLoading />;
|
||||
|
||||
if (showError.show)
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request')}
|
||||
@@ -143,6 +172,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
variant="contained"
|
||||
>
|
||||
{/* add new lesson type */}
|
||||
{/* TODO: refactor translations.json */}
|
||||
{t('dashboard.lessonTypes.add')}
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
@@ -153,14 +183,12 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
filters={{ email, phone, status, name, visible, type }}
|
||||
fullData={lessonTypesData}
|
||||
sortDir={sortDir}
|
||||
//
|
||||
/>
|
||||
<Divider />
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<LessonTypesTable
|
||||
reloadRows={reloadRows}
|
||||
rows={f}
|
||||
//
|
||||
/>
|
||||
</Box>
|
||||
<Divider />
|
||||
@@ -174,6 +202,9 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
</Card>
|
||||
</LessonTypesSelectionProvider>
|
||||
</Stack>
|
||||
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
|
||||
<pre>{JSON.stringify(f, null, 2)}</pre>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -235,6 +266,5 @@ interface PageProps {
|
||||
name?: string;
|
||||
visible?: string;
|
||||
type?: string;
|
||||
//
|
||||
};
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PropertyItem } from '@/components/core/property-item';
|
||||
import { PropertyList } from '@/components/core/property-list';
|
||||
import { LpCategory } from '@/components/dashboard/lp_categories/type';
|
||||
import { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||
|
||||
export default function BasicDetailCard({
|
||||
lpModel: model,
|
||||
|
@@ -10,7 +10,7 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
|
||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { LpCategory } from '@/components/dashboard/lp_categories/type';
|
||||
import { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||
|
||||
function getImageUrlFrRecord(record: LpCategory): string {
|
||||
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
||||
|
@@ -5,8 +5,8 @@ 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/SamplePaymentCard';
|
||||
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
|
||||
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
|
||||
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
|
||||
import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
@@ -21,9 +21,9 @@ import { logger } from '@/lib/default-logger';
|
||||
import { pb } from '@/lib/pb';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
import ErrorDisplay from '@/components/dashboard/error';
|
||||
import { defaultLpCategory } from '@/components/dashboard/lp_categories/_constants.ts';
|
||||
import { Notifications } from '@/components/dashboard/lp_categories/notifications';
|
||||
import type { LpCategory } from '@/components/dashboard/lp_categories/type';
|
||||
import { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants.ts';
|
||||
import { Notifications } from '@/components/dashboard/lp/categories/notifications';
|
||||
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
import BasicDetailCard from './BasicDetailCard';
|
||||
|
@@ -13,7 +13,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { LpCategoryCreateForm } from '@/components/dashboard/lp_categories/lp-category-create-form';
|
||||
import { LpCategoryCreateForm } from '@/components/dashboard/lp/categories/lp-category-create-form';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
// RULES: follow the name of page directory
|
||||
|
@@ -10,7 +10,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { LpCategoryEditForm } from '@/components/dashboard/lp_categories/lp-category-edit-form';
|
||||
import { LpCategoryEditForm } from '@/components/dashboard/lp/categories/lp-category-edit-form';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
const { t } = useTranslation(['lp_categories']);
|
||||
|
@@ -23,13 +23,13 @@ import { logger } from '@/lib/default-logger';
|
||||
import { pb } from '@/lib/pb';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
import ErrorDisplay from '@/components/dashboard/error';
|
||||
import { defaultLpCategory } from '@/components/dashboard/lp_categories/_constants';
|
||||
import { LpCategoriesFilters } from '@/components/dashboard/lp_categories/lp-categories-filters';
|
||||
import type { Filters } from '@/components/dashboard/lp_categories/lp-categories-filters';
|
||||
import { LpCategoriesPagination } from '@/components/dashboard/lp_categories/lp-categories-pagination';
|
||||
import { LpCategoriesSelectionProvider } from '@/components/dashboard/lp_categories/lp-categories-selection-context';
|
||||
import { LpCategoriesTable } from '@/components/dashboard/lp_categories/lp-categories-table';
|
||||
import type { LpCategory } from '@/components/dashboard/lp_categories/type';
|
||||
import { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants';
|
||||
import { LpCategoriesFilters } from '@/components/dashboard/lp/categories/lp-categories-filters';
|
||||
import type { Filters } from '@/components/dashboard/lp/categories/lp-categories-filters';
|
||||
import { LpCategoriesPagination } from '@/components/dashboard/lp/categories/lp-categories-pagination';
|
||||
import { LpCategoriesSelectionProvider } from '@/components/dashboard/lp/categories/lp-categories-selection-context';
|
||||
import { LpCategoriesTable } from '@/components/dashboard/lp/categories/lp-categories-table';
|
||||
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
@@ -44,8 +44,10 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||
//
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||
//
|
||||
const [f, setF] = React.useState<LpCategory[]>([]);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(1);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||
//
|
||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||
const [listOption, setListOption] = React.useState({});
|
||||
const [listSort, setListSort] = React.useState({});
|
||||
@@ -54,6 +56,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
|
||||
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status });
|
||||
|
||||
//
|
||||
const reloadRows = async (): Promise<void> => {
|
||||
try {
|
||||
const models: ListResult<RecordModel> = await pb
|
||||
@@ -67,26 +70,81 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
setLessonCategoriesData(tempLessonTypes);
|
||||
setRecordCount(totalItems);
|
||||
setF(tempLessonTypes);
|
||||
console.log({ currentPage, f });
|
||||
// console.log({ currentPage, f });
|
||||
} catch (error) {
|
||||
//
|
||||
setShowError({ show: true, detail: JSON.stringify(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(() => {
|
||||
void reloadRows();
|
||||
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 <FormLoading />;
|
||||
|
||||
if (showError.show)
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request')}
|
||||
code="500"
|
||||
code={-1}
|
||||
details={showError.detail}
|
||||
/>
|
||||
);
|
||||
|
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PropertyItem } from '@/components/core/property-item';
|
||||
import { PropertyList } from '@/components/core/property-list';
|
||||
import { LpCategory } from '@/components/dashboard/lp_categories/type';
|
||||
import { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||
|
||||
export default function BasicDetailCard({
|
||||
lpModel: model,
|
||||
|
@@ -10,7 +10,7 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
|
||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { LpCategory } from '@/components/dashboard/lp_categories/type';
|
||||
import { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||
|
||||
function getImageUrlFrRecord(record: LpCategory): string {
|
||||
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
||||
|
@@ -5,8 +5,8 @@ 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/SamplePaymentCard';
|
||||
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
|
||||
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
|
||||
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
|
||||
import { COL_QUIZ_LP_QUESTIONS } from '@/constants';
|
||||
import { Grid } from '@mui/material';
|
||||
import Box from '@mui/material/Box';
|
||||
@@ -21,9 +21,9 @@ import { logger } from '@/lib/default-logger';
|
||||
import { pb } from '@/lib/pb';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
import ErrorDisplay from '@/components/dashboard/error';
|
||||
import { defaultLpQuestion } from '@/components/dashboard/lp_questions/_constants.ts';
|
||||
import { Notifications } from '@/components/dashboard/lp_questions/notifications';
|
||||
import type { LpQuestion } from '@/components/dashboard/lp_questions/type';
|
||||
import { defaultLpQuestion } from '@/components/dashboard/lp/questions/_constants.ts';
|
||||
import { Notifications } from '@/components/dashboard/lp/questions/notifications';
|
||||
import type { LpQuestion } from '@/components/dashboard/lp/questions/type';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
import BasicDetailCard from './BasicDetailCard';
|
||||
|
@@ -13,7 +13,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { LpQuestionCreateForm } from '@/components/dashboard/lp_questions/lp-question-create-form';
|
||||
import { LpQuestionCreateForm } from '@/components/dashboard/lp/questions/lp-question-create-form';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
// RULES: follow the name of page directory
|
||||
|
@@ -10,7 +10,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { LpQuestionEditForm } from '@/components/dashboard/lp_questions/lp-question-edit-form';
|
||||
import { LpQuestionEditForm } from '@/components/dashboard/lp/questions/lp-question-edit-form';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
const { t } = useTranslation(['lp_questions']);
|
||||
|
@@ -23,13 +23,13 @@ import { logger } from '@/lib/default-logger';
|
||||
import { pb } from '@/lib/pb';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
import ErrorDisplay from '@/components/dashboard/error';
|
||||
import { defaultLpQuestion } from '@/components/dashboard/lp_questions/_constants';
|
||||
import { LpQuestionsFilters } from '@/components/dashboard/lp_questions/lp-questions-filters';
|
||||
import type { Filters } from '@/components/dashboard/lp_questions/lp-questions-filters';
|
||||
import { LpQuestionsPagination } from '@/components/dashboard/lp_questions/lp-questions-pagination';
|
||||
import { LpQuestionsSelectionProvider } from '@/components/dashboard/lp_questions/lp-questions-selection-context';
|
||||
import { LpQuestionsTable } from '@/components/dashboard/lp_questions/lp-questions-table';
|
||||
import type { LpQuestion } from '@/components/dashboard/lp_questions/type';
|
||||
import { defaultLpQuestion } from '@/components/dashboard/lp/questions/_constants';
|
||||
import { LpQuestionsFilters } from '@/components/dashboard/lp/questions/lp-questions-filters';
|
||||
import type { Filters } from '@/components/dashboard/lp/questions/lp-questions-filters';
|
||||
import { LpQuestionsPagination } from '@/components/dashboard/lp/questions/lp-questions-pagination';
|
||||
import { LpQuestionsSelectionProvider } from '@/components/dashboard/lp/questions/lp-questions-selection-context';
|
||||
import { LpQuestionsTable } from '@/components/dashboard/lp/questions/lp-questions-table';
|
||||
import type { LpQuestion } from '@/components/dashboard/lp/questions/type';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
@@ -44,8 +44,10 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||
//
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||
//
|
||||
const [f, setF] = React.useState<LpQuestion[]>([]);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||
//
|
||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||
const [listOption, setListOption] = React.useState({});
|
||||
const [listSort, setListSort] = React.useState({});
|
||||
|
@@ -5,8 +5,8 @@ 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/SamplePaymentCard';
|
||||
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
|
||||
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';
|
||||
|
@@ -13,7 +13,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { MfCategoryCreateForm } from '@/components/dashboard/mf/categories/lp-category-create-form';
|
||||
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
|
||||
|
@@ -33,8 +33,7 @@ import type { MfCategory } from '@/components/dashboard/mf/categories/type';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
// TODO: modify from lp_categories to mf_categories
|
||||
const { t } = useTranslation(['lp_categories']);
|
||||
const { t } = useTranslation(['mf_categories']);
|
||||
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||
const router = useRouter();
|
||||
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<MfCategory[]>([]);
|
||||
@@ -59,7 +58,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
try {
|
||||
const models: ListResult<RecordModel> = await pb
|
||||
.collection(COL_QUIZ_MF_CATEGORIES)
|
||||
.getList(currentPage + 1, rowsPerPage, {});
|
||||
.getList(currentPage + 1, rowsPerPage, listOption);
|
||||
const { items, totalItems } = models;
|
||||
const tempLessonTypes: MfCategory[] = items.map((lt) => {
|
||||
return { ...defaultMfCategory, ...lt };
|
||||
@@ -68,26 +67,81 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
setLessonCategoriesData(tempLessonTypes);
|
||||
setRecordCount(totalItems);
|
||||
setF(tempLessonTypes);
|
||||
console.log({ currentPage, f });
|
||||
// console.log({ currentPage, f });
|
||||
} catch (error) {
|
||||
//
|
||||
setShowError({ show: true, detail: JSON.stringify(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(() => {
|
||||
void reloadRows();
|
||||
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 <FormLoading />;
|
||||
|
||||
if (showError.show)
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request')}
|
||||
code="500"
|
||||
code={-1}
|
||||
details={showError.detail}
|
||||
/>
|
||||
);
|
||||
|
@@ -5,8 +5,8 @@ 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/SamplePaymentCard';
|
||||
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
|
||||
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';
|
||||
|
@@ -33,7 +33,7 @@ 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(['lp_questions']);
|
||||
const { t } = useTranslation(['mf_question']);
|
||||
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||
const router = useRouter();
|
||||
const [lessonQuestionsData, setLessonCategoriesData] = React.useState<MfQuestion[]>([]);
|
||||
|
1663
002_source/cms/src/app/dashboard/mf/repomix-output.xml
Normal file
1663
002_source/cms/src/app/dashboard/mf/repomix-output.xml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
||||
# CR Categories Components Summary
|
||||
|
||||
## Main Components
|
||||
|
||||
### `cr-categories-table.tsx`
|
||||
|
||||
- Displays categories in a table format with columns for Name, Status, Created At, etc.
|
||||
- Features:
|
||||
- Row selection functionality
|
||||
- Status indicators (Active/Blocked/Pending)
|
||||
- Progress bars for quota/word count
|
||||
- Edit/delete actions
|
||||
- Image and name display with slugs
|
||||
|
||||
### `cr-category-create-form.tsx`
|
||||
|
||||
- Form for creating new categories
|
||||
- Fields:
|
||||
- Name, image, position, visibility
|
||||
- Slug, description, remarks
|
||||
- Initial answer (JSON)
|
||||
- Uses Zod validation and React Hook Form
|
||||
- Material UI components
|
||||
- Internationalization support
|
||||
|
||||
### `cr-category-edit-form.tsx`
|
||||
|
||||
- Similar to create form but for editing
|
||||
- Pre-fills existing data
|
||||
- Handles image updates
|
||||
- More strict validation for init_answer
|
||||
|
||||
## Supporting Components
|
||||
|
||||
### `confirm-delete-modal.tsx`
|
||||
|
||||
- Confirmation dialog for category deletion
|
||||
- Loading states and toast notifications
|
||||
- Internationalization support
|
||||
|
||||
### `cr-categories-filters.tsx`
|
||||
|
||||
- Filtering functionality:
|
||||
- Visibility status tabs
|
||||
- Text search filters
|
||||
- Sorting options
|
||||
- Shows selected items count
|
||||
|
||||
### `cr-categories-pagination.tsx`
|
||||
|
||||
- Basic pagination controls
|
||||
- Page number and rows per page selection
|
||||
- Default options: [5, 10, 25]
|
||||
|
||||
### `cr-categories-selection-context.tsx`
|
||||
|
||||
- Manages selection state
|
||||
- Provides hooks for:
|
||||
- Selecting/deselecting items
|
||||
- Checking selection state
|
||||
- Bulk operations
|
||||
|
||||
## Types & Constants
|
||||
|
||||
### `type.d.ts`
|
||||
|
||||
- Interfaces:
|
||||
- `CrCategory`: Main category type
|
||||
- `CreateFormProps`: Create form data
|
||||
- `EditFormProps`: Edit form data
|
||||
|
||||
### `_constants.ts`
|
||||
|
||||
- Default values:
|
||||
- `defaultCrCategory`
|
||||
- `emptyCrCategory`
|
||||
|
||||
## Component Relationships
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[cr-categories-table] --> B[cr-category-create-form]
|
||||
A --> C[cr-category-edit-form]
|
||||
A --> D[confirm-delete-modal]
|
||||
A --> E[cr-categories-filters]
|
||||
A --> F[cr-categories-pagination]
|
||||
A --> G[cr-categories-selection-context]
|
||||
H[type.d.ts] --> A
|
||||
H --> B
|
||||
H --> C
|
||||
I[_constants.ts] --> A
|
||||
I --> B
|
||||
I --> C
|
||||
```
|
@@ -1,8 +1,8 @@
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
|
||||
import { CreateFormProps, MfCategory } from './type';
|
||||
import { CrCategory, CreateFormProps } from './type';
|
||||
|
||||
export const defaultLpCategory: MfCategory = {
|
||||
export const defaultCrCategory: CrCategory = {
|
||||
isEmpty: false,
|
||||
id: 'default-id',
|
||||
cat_name: 'default-category-name',
|
||||
@@ -38,7 +38,7 @@ export const defaultLpCategory: MfCategory = {
|
||||
// imageUrl: '',
|
||||
// };
|
||||
|
||||
export const emptyLpCategory: MfCategory = {
|
||||
...defaultLpCategory,
|
||||
export const emptyCrCategory: CrCategory = {
|
||||
...defaultCrCategory,
|
||||
isEmpty: true,
|
||||
};
|
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import deleteQuizMFCategories from '@/db/QuizMFCategories/Delete';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { COL_LESSON_TYPES } from '@/constants';
|
||||
import deleteQuizCRCategories from '@/db/QuizCRCategories/Delete';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { Button, Container, Modal, Paper } from '@mui/material';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
@@ -44,7 +46,9 @@ export default function ConfirmDeleteModal({
|
||||
function handleUserConfirmDelete(): void {
|
||||
if (idToDelete) {
|
||||
setIsDeleteing(true);
|
||||
deleteQuizMFCategories(idToDelete)
|
||||
|
||||
// RULES: CR -> deleteQuizCRCategories
|
||||
deleteQuizCRCategories(idToDelete)
|
||||
.then(() => {
|
||||
reloadRows();
|
||||
handleClose();
|
@@ -3,9 +3,12 @@
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
// import { COL_LESSON_CATEGORIES } from '@/constants';
|
||||
|
||||
// RULES: Quiz
|
||||
import GetAllCount from '@/db/QuizMFCategories/GetAllCount';
|
||||
import GetHiddenCount from '@/db/QuizMFCategories/GetHiddenCount';
|
||||
import GetVisibleCount from '@/db/QuizMFCategories/GetVisibleCount';
|
||||
//
|
||||
import Button from '@mui/material/Button';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Divider from '@mui/material/Divider';
|
||||
@@ -23,8 +26,8 @@ import { paths } from '@/paths';
|
||||
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
|
||||
import { Option } from '@/components/core/option';
|
||||
|
||||
import { useMfCategoriesSelection } from './mf-categories-selection-context';
|
||||
import type { MfCategory } from './type';
|
||||
import { useCrCategoriesSelection } from './cr-categories-selection-context';
|
||||
import type { CrCategory } from './type';
|
||||
|
||||
export interface Filters {
|
||||
email?: string;
|
||||
@@ -37,17 +40,17 @@ export interface Filters {
|
||||
|
||||
export type SortDir = 'asc' | 'desc';
|
||||
|
||||
export interface LpCategoriesFiltersProps {
|
||||
export interface CrCategoriesFiltersProps {
|
||||
filters?: Filters;
|
||||
sortDir?: SortDir;
|
||||
fullData: MfCategory[];
|
||||
fullData: CrCategory[];
|
||||
}
|
||||
|
||||
export function LpCategoriesFilters({
|
||||
export function CrCategoriesFilters({
|
||||
filters = {},
|
||||
sortDir = 'desc',
|
||||
fullData,
|
||||
}: LpCategoriesFiltersProps): React.JSX.Element {
|
||||
}: CrCategoriesFiltersProps): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const { email, phone, status, name, visible, type } = filters;
|
||||
|
||||
@@ -57,16 +60,16 @@ export function LpCategoriesFilters({
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const selection = useMfCategoriesSelection();
|
||||
const selection = useCrCategoriesSelection();
|
||||
|
||||
function getVisible(): number {
|
||||
return fullData.reduce((count, item: MfCategory) => {
|
||||
return fullData.reduce((count, item: CrCategory) => {
|
||||
return item.visible === 'visible' ? count + 1 : count;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function getHidden(): number {
|
||||
return fullData.reduce((count, item: MfCategory) => {
|
||||
return fullData.reduce((count, item: CrCategory) => {
|
||||
return item.visible === 'hidden' ? count + 1 : count;
|
||||
}, 0);
|
||||
}
|
||||
@@ -114,7 +117,7 @@ export function LpCategoriesFilters({
|
||||
}
|
||||
|
||||
// NOTE: modify according to COLLECTION
|
||||
router.push(`${paths.dashboard.mf_categories.list}?${searchParams.toString()}`);
|
||||
router.push(`${paths.dashboard.lp_categories.list}?${searchParams.toString()}`);
|
||||
},
|
||||
[router]
|
||||
);
|
@@ -1,5 +1,8 @@
|
||||
'use client';
|
||||
|
||||
// lp-categories-pagination.tsx
|
||||
// RULES:
|
||||
// T.B.A.
|
||||
import * as React from 'react';
|
||||
import TablePagination from '@mui/material/TablePagination';
|
||||
|
||||
@@ -7,7 +10,7 @@ function noop(): void {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
interface LessonCategoriesPaginationProps {
|
||||
interface CrCategoriesPaginationProps {
|
||||
count: number;
|
||||
page: number;
|
||||
//
|
||||
@@ -16,14 +19,14 @@ interface LessonCategoriesPaginationProps {
|
||||
rowsPerPage: number;
|
||||
}
|
||||
|
||||
export function LpCategoriesPagination({
|
||||
export function CrCategoriesPagination({
|
||||
count,
|
||||
page,
|
||||
//
|
||||
setPage,
|
||||
setRowsPerPage,
|
||||
rowsPerPage,
|
||||
}: LessonCategoriesPaginationProps): React.JSX.Element {
|
||||
}: CrCategoriesPaginationProps): 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) => {
|
@@ -6,15 +6,15 @@ import * as React from 'react';
|
||||
import { useSelection } from '@/hooks/use-selection';
|
||||
import type { Selection } from '@/hooks/use-selection';
|
||||
|
||||
import { LpCategory } from './type';
|
||||
import { CrCategory } from './type';
|
||||
|
||||
function noop(): void {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export interface LpCategoriesSelectionContextValue extends Selection {}
|
||||
export interface CrCategoriesSelectionContextValue extends Selection {}
|
||||
|
||||
export const LpCategoriesSelectionContext = React.createContext<LpCategoriesSelectionContextValue>({
|
||||
export const CrCategoriesSelectionContext = React.createContext<CrCategoriesSelectionContextValue>({
|
||||
deselectAll: noop,
|
||||
deselectOne: noop,
|
||||
selectAll: noop,
|
||||
@@ -26,10 +26,10 @@ export const LpCategoriesSelectionContext = React.createContext<LpCategoriesSele
|
||||
|
||||
interface LpCategoriesSelectionProviderProps {
|
||||
children: React.ReactNode;
|
||||
lessonCategories: LpCategory[];
|
||||
lessonCategories: CrCategory[];
|
||||
}
|
||||
|
||||
export function LpCategoriesSelectionProvider({
|
||||
export function CrCategoriesSelectionProvider({
|
||||
children,
|
||||
lessonCategories = [],
|
||||
}: LpCategoriesSelectionProviderProps): React.JSX.Element {
|
||||
@@ -37,10 +37,10 @@ export function LpCategoriesSelectionProvider({
|
||||
const selection = useSelection(customerIds);
|
||||
|
||||
return (
|
||||
<LpCategoriesSelectionContext.Provider value={{ ...selection }}>{children}</LpCategoriesSelectionContext.Provider>
|
||||
<CrCategoriesSelectionContext.Provider value={{ ...selection }}>{children}</CrCategoriesSelectionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLpCategoriesSelection(): LpCategoriesSelectionContextValue {
|
||||
return React.useContext(LpCategoriesSelectionContext);
|
||||
export function useCrCategoriesSelection(): CrCategoriesSelectionContextValue {
|
||||
return React.useContext(CrCategoriesSelectionContext);
|
||||
}
|
@@ -25,10 +25,10 @@ import { DataTable } from '@/components/core/data-table';
|
||||
import type { ColumnDef } from '@/components/core/data-table';
|
||||
|
||||
import ConfirmDeleteModal from './confirm-delete-modal';
|
||||
import { useLpCategoriesSelection } from './lp-categories-selection-context';
|
||||
import type { LpCategory } from './type';
|
||||
import { useCrCategoriesSelection } from './cr-categories-selection-context';
|
||||
import type { CrCategory } from './type';
|
||||
|
||||
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCategory>[] {
|
||||
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<CrCategory>[] {
|
||||
return [
|
||||
{
|
||||
formatter: (row): React.JSX.Element => (
|
||||
@@ -40,7 +40,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCateg
|
||||
<Link
|
||||
color="inherit"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.lp_categories.details(row.id)}
|
||||
href={paths.dashboard.cr_categories.details(row.id)}
|
||||
sx={{ whiteSpace: 'nowrap' }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
@@ -163,7 +163,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCateg
|
||||
//
|
||||
color="secondary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.lp_categories.details(row.id)}
|
||||
href={paths.dashboard.cr_categories.details(row.id)}
|
||||
>
|
||||
<PencilSimpleIcon size={24} />
|
||||
</LoadingButton>
|
||||
@@ -187,13 +187,13 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCateg
|
||||
}
|
||||
|
||||
export interface LessonCategoriesTableProps {
|
||||
rows: LpCategory[];
|
||||
rows: CrCategory[];
|
||||
reloadRows: () => void;
|
||||
}
|
||||
|
||||
export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element {
|
||||
export function CrCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element {
|
||||
const { t } = useTranslation(['lp_categories']);
|
||||
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useLpCategoriesSelection();
|
||||
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCrCategoriesSelection();
|
||||
|
||||
const [idToDelete, setIdToDelete] = React.useState('');
|
||||
const [open, setOpen] = React.useState(false);
|
||||
@@ -211,7 +211,7 @@ export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTablePro
|
||||
reloadRows={reloadRows}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
<DataTable<LpCategory>
|
||||
<DataTable<CrCategory>
|
||||
columns={columns(handleDeleteClick)}
|
||||
onDeselectAll={deselectAll}
|
||||
onDeselectOne={(_, row) => {
|
||||
@@ -232,6 +232,7 @@ export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTablePro
|
||||
sx={{ textAlign: 'center' }}
|
||||
variant="body2"
|
||||
>
|
||||
{/* TODO: update this */}
|
||||
{t('no-lesson-categories-found')}
|
||||
</Typography>
|
||||
</Box>
|
@@ -66,7 +66,7 @@ export const defaultValues = {
|
||||
description: '',
|
||||
} satisfies Values;
|
||||
|
||||
export function LpCategoryCreateForm(): React.JSX.Element {
|
||||
export function CrCategoryCreateForm(): React.JSX.Element {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation(['lp_categories']);
|
||||
|
@@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
import RouterLink from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
//
|
||||
import { COL_QUIZ_MF_CATEGORIES } from '@/constants';
|
||||
import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
//
|
||||
@@ -39,7 +39,7 @@ import { TextEditor } from '@/components/core/text-editor/text-editor';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
import ErrorDisplay from '../error';
|
||||
import ErrorDisplay from '../../error';
|
||||
import type { EditFormProps } from './type';
|
||||
|
||||
// TODO: review this
|
||||
@@ -88,7 +88,7 @@ const defaultValues = {
|
||||
description: '',
|
||||
} satisfies Values;
|
||||
|
||||
export function LpCategoryEditForm(): React.JSX.Element {
|
||||
export function CrCategoryEditForm(): React.JSX.Element {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation(['lp_categories']);
|
||||
|
||||
@@ -129,10 +129,10 @@ export function LpCategoryEditForm(): React.JSX.Element {
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).update(catId, tempUpdate);
|
||||
const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).update(catId, tempUpdate);
|
||||
logger.debug(result);
|
||||
toast.success(t('edit.success'));
|
||||
router.push(paths.dashboard.mf_categories.list);
|
||||
router.push(paths.dashboard.cr_categories.list);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
toast.error(t('update.failed'));
|
||||
@@ -171,7 +171,7 @@ export function LpCategoryEditForm(): React.JSX.Element {
|
||||
setShowLoading(true);
|
||||
|
||||
try {
|
||||
const result = await pb.collection(COL_QUIZ_MF_CATEGORIES).getOne(id);
|
||||
const result = await pb.collection(COL_QUIZ_LP_CATEGORIES).getOne(id);
|
||||
|
||||
reset({ ...defaultValues, ...result, init_answer: JSON.stringify(result.init_answer) });
|
||||
setTextDescription(result.description);
|
||||
@@ -480,7 +480,7 @@ export function LpCategoryEditForm(): React.JSX.Element {
|
||||
<Button
|
||||
color="secondary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.mf_categories.list}
|
||||
href={paths.dashboard.cr_categories.list}
|
||||
>
|
||||
{t('edit.cancelButton')}
|
||||
</Button>
|
@@ -1,4 +1,4 @@
|
||||
export interface LpCategory {
|
||||
export interface CrCategory {
|
||||
isEmpty?: boolean;
|
||||
//
|
||||
id: string;
|
@@ -1,11 +1,11 @@
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
|
||||
import { CreateFormProps, LpQuestion } from './type';
|
||||
import { CreateFormProps, CrQuestion } from './type';
|
||||
|
||||
export const defaultLpQuestion: LpQuestion = {
|
||||
export const defaultCrQuestion: CrQuestion = {
|
||||
isEmpty: false,
|
||||
id: 'default-id',
|
||||
cat_name: 'default-category-name',
|
||||
cat_name: 'default-question-name',
|
||||
cat_image_url: undefined,
|
||||
cat_image: undefined,
|
||||
pos: 0,
|
||||
@@ -38,7 +38,7 @@ export const defaultLpQuestion: LpQuestion = {
|
||||
// imageUrl: '',
|
||||
// };
|
||||
|
||||
export const emptyLpCategory: LpQuestion = {
|
||||
...defaultLpQuestion,
|
||||
export const emptyLpQuestion: CrQuestion = {
|
||||
...defaultCrQuestion,
|
||||
isEmpty: true,
|
||||
};
|
@@ -66,7 +66,7 @@ export const defaultValues = {
|
||||
description: '',
|
||||
} satisfies Values;
|
||||
|
||||
export function MfCategoryCreateForm(): React.JSX.Element {
|
||||
export function CrQuestionCreateForm(): React.JSX.Element {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation(['lp_categories']);
|
||||
|
||||
@@ -105,7 +105,7 @@ export function MfCategoryCreateForm(): React.JSX.Element {
|
||||
|
||||
logger.debug(result);
|
||||
toast.success(t('create.success'));
|
||||
router.push(paths.dashboard.mf_categories.list);
|
||||
router.push(paths.dashboard.lp_categories.list);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
toast.error(t('create.failed'));
|
@@ -39,7 +39,7 @@ import { TextEditor } from '@/components/core/text-editor/text-editor';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
import ErrorDisplay from '../error';
|
||||
import ErrorDisplay from '../../error';
|
||||
import type { EditFormProps } from './type';
|
||||
|
||||
// TODO: review this
|
||||
@@ -88,7 +88,7 @@ const defaultValues = {
|
||||
description: '',
|
||||
} satisfies Values;
|
||||
|
||||
export function LpCategoryEditForm(): React.JSX.Element {
|
||||
export function CrQuestionEditForm(): React.JSX.Element {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation(['lp_categories']);
|
||||
|
@@ -23,8 +23,8 @@ import { paths } from '@/paths';
|
||||
import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button';
|
||||
import { Option } from '@/components/core/option';
|
||||
|
||||
import { useLpCategoriesSelection } from './lp-questions-selection-context';
|
||||
import { LpQuestion } from './type';
|
||||
import { useCrQuestionsSelection } from './cr-questions-selection-context';
|
||||
import { CrQuestion } from './type';
|
||||
|
||||
export interface Filters {
|
||||
email?: string;
|
||||
@@ -37,17 +37,17 @@ export interface Filters {
|
||||
|
||||
export type SortDir = 'asc' | 'desc';
|
||||
|
||||
export interface LpCategoriesFiltersProps {
|
||||
export interface LpQuestionsFiltersProps {
|
||||
filters?: Filters;
|
||||
sortDir?: SortDir;
|
||||
fullData: LpQuestion[];
|
||||
fullData: CrQuestion[];
|
||||
}
|
||||
|
||||
export function LpQuestionsFilters({
|
||||
export function CrQuestionsFilters({
|
||||
filters = {},
|
||||
sortDir = 'desc',
|
||||
fullData,
|
||||
}: LpCategoriesFiltersProps): React.JSX.Element {
|
||||
}: LpQuestionsFiltersProps): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const { email, phone, status, name, visible, type } = filters;
|
||||
|
||||
@@ -57,16 +57,16 @@ export function LpQuestionsFilters({
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const selection = useLpCategoriesSelection();
|
||||
const selection = useCrQuestionsSelection();
|
||||
|
||||
function getVisible(): number {
|
||||
return fullData.reduce((count, item: LpQuestion) => {
|
||||
return fullData.reduce((count, item: CrQuestion) => {
|
||||
return item.visible === 'visible' ? count + 1 : count;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function getHidden(): number {
|
||||
return fullData.reduce((count, item: LpQuestion) => {
|
||||
return fullData.reduce((count, item: CrQuestion) => {
|
||||
return item.visible === 'hidden' ? count + 1 : count;
|
||||
}, 0);
|
||||
}
|
@@ -7,7 +7,7 @@ function noop(): void {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
interface LessonCategoriesPaginationProps {
|
||||
interface LessonQuestionsPaginationProps {
|
||||
count: number;
|
||||
page: number;
|
||||
//
|
||||
@@ -16,14 +16,14 @@ interface LessonCategoriesPaginationProps {
|
||||
rowsPerPage: number;
|
||||
}
|
||||
|
||||
export function LpQuestionsPagination({
|
||||
export function CrQuestionsPagination({
|
||||
count,
|
||||
page,
|
||||
//
|
||||
setPage,
|
||||
setRowsPerPage,
|
||||
rowsPerPage,
|
||||
}: LessonCategoriesPaginationProps): React.JSX.Element {
|
||||
}: LessonQuestionsPaginationProps): React.JSX.Element {
|
||||
// You should implement the pagination using a similar logic as the filters.
|
||||
// Note that when page change, you should keep the filter search params.
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
// import type { LessonCategory } from '@/types/lesson-type';
|
||||
import { useSelection } from '@/hooks/use-selection';
|
||||
import type { Selection } from '@/hooks/use-selection';
|
||||
|
||||
import type { CrQuestion } from './type';
|
||||
|
||||
function noop(): void {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export interface CrQuestionsSelectionContextValue extends Selection {}
|
||||
|
||||
export const CrQuestionsSelectionContext = React.createContext<CrQuestionsSelectionContextValue>({
|
||||
deselectAll: noop,
|
||||
deselectOne: noop,
|
||||
selectAll: noop,
|
||||
selectOne: noop,
|
||||
selected: new Set(),
|
||||
selectedAny: false,
|
||||
selectedAll: false,
|
||||
});
|
||||
|
||||
interface CrQuestionsSelectionProviderProps {
|
||||
children: React.ReactNode;
|
||||
lessonQuestions: CrQuestion[];
|
||||
}
|
||||
|
||||
export function CrQuestionsSelectionProvider({
|
||||
children,
|
||||
lessonQuestions = [],
|
||||
}: CrQuestionsSelectionProviderProps): React.JSX.Element {
|
||||
const customerIds = React.useMemo(() => lessonQuestions.map((customer) => customer.id), [lessonQuestions]);
|
||||
const selection = useSelection(customerIds);
|
||||
|
||||
return (
|
||||
<CrQuestionsSelectionContext.Provider value={{ ...selection }}>{children}</CrQuestionsSelectionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useCrQuestionsSelection(): CrQuestionsSelectionContextValue {
|
||||
return React.useContext(CrQuestionsSelectionContext);
|
||||
}
|
@@ -6,7 +6,6 @@ import { LoadingButton } from '@mui/lab';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
@@ -26,10 +25,10 @@ import { DataTable } from '@/components/core/data-table';
|
||||
import type { ColumnDef } from '@/components/core/data-table';
|
||||
|
||||
import ConfirmDeleteModal from './confirm-delete-modal';
|
||||
import { useMfCategoriesSelection } from './mf-categories-selection-context';
|
||||
import type { MfCategory } from './type';
|
||||
import { useCrQuestionsSelection } from './cr-questions-selection-context';
|
||||
import type { CrQuestion } from './type';
|
||||
|
||||
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<MfCategory>[] {
|
||||
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<CrQuestion>[] {
|
||||
return [
|
||||
{
|
||||
formatter: (row): React.JSX.Element => (
|
||||
@@ -41,7 +40,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<MfCateg
|
||||
<Link
|
||||
color="inherit"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.mf_categories.details(row.id)}
|
||||
href={paths.dashboard.cr_questions.details(row.id)}
|
||||
sx={{ whiteSpace: 'nowrap' }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
@@ -164,7 +163,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<MfCateg
|
||||
//
|
||||
color="secondary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.mf_categories.details(row.id)}
|
||||
href={paths.dashboard.cr_questions.details(row.id)}
|
||||
>
|
||||
<PencilSimpleIcon size={24} />
|
||||
</LoadingButton>
|
||||
@@ -187,14 +186,14 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<MfCateg
|
||||
];
|
||||
}
|
||||
|
||||
export interface LessonCategoriesTableProps {
|
||||
rows: MfCategory[];
|
||||
export interface LessonQuestionsTableProps {
|
||||
rows: CrQuestion[];
|
||||
reloadRows: () => void;
|
||||
}
|
||||
|
||||
export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTableProps): React.JSX.Element {
|
||||
export function CrQuestionsTable({ rows, reloadRows }: LessonQuestionsTableProps): React.JSX.Element {
|
||||
const { t } = useTranslation(['lp_categories']);
|
||||
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useMfCategoriesSelection();
|
||||
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCrQuestionsSelection();
|
||||
|
||||
const [idToDelete, setIdToDelete] = React.useState('');
|
||||
const [open, setOpen] = React.useState(false);
|
||||
@@ -212,7 +211,7 @@ export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTablePro
|
||||
reloadRows={reloadRows}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
<DataTable<MfCategory>
|
||||
<DataTable<CrQuestion>
|
||||
columns={columns(handleDeleteClick)}
|
||||
onDeselectAll={deselectAll}
|
||||
onDeselectOne={(_, row) => {
|
||||
@@ -233,6 +232,7 @@ export function LpCategoriesTable({ rows, reloadRows }: LessonCategoriesTablePro
|
||||
sx={{ textAlign: 'center' }}
|
||||
variant="body2"
|
||||
>
|
||||
{/* TODO: update this */}
|
||||
{t('no-lesson-categories-found')}
|
||||
</Typography>
|
||||
</Box>
|
@@ -1,4 +1,4 @@
|
||||
export interface MfCategory {
|
||||
export interface CrQuestion {
|
||||
isEmpty?: boolean;
|
||||
//
|
||||
id: string;
|
4642
002_source/cms/src/components/dashboard/cr/repomix-output.xml
Normal file
4642
002_source/cms/src/components/dashboard/cr/repomix-output.xml
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user