Compare commits

...

27 Commits

Author SHA1 Message Date
louiscklaw
39a7d32fcd update, 2025-05-11 15:47:49 +08:00
louiscklaw
85d1ecdc90 update, 2025-05-11 15:44:18 +08:00
louiscklaw
b26e1ff167 update user-button, 2025-05-11 13:47:14 +08:00
louiscklaw
de415a37bc update main-nav and side-nav, 2025-05-11 13:39:33 +08:00
louiscklaw
01a8d2ca02 update main-nav, 2025-05-11 13:24:52 +08:00
louiscklaw
e5b136b8b5 update main-nav refactoring, working, 2025-05-11 13:06:58 +08:00
louiscklaw
031dbed6a9 "implement responsive main navigation bar with dropdown menus and mobile view support" 2025-05-11 13:00:30 +08:00
louiscklaw
f20dfa00c2 "add CMS workspace configuration with linked documentation and AI workspace folders" 2025-05-11 13:00:05 +08:00
louiscklaw
24c91cb6f0 "update gitignore to exclude temporary and backup files" 2025-05-11 10:48:45 +08:00
louiscklaw
abca91c26a "introduce horizontal main navigation component with dynamic styling and interactive elements" 2025-05-11 10:39:09 +08:00
louiscklaw
3321eafffa "update gitignore to exclude draft files" 2025-05-11 10:36:46 +08:00
louiscklaw
adc9275d3f "update prettier config, add TODO list, and create workspace configuration" 2025-05-11 10:35:04 +08:00
louiscklaw
60eed00cb2 "add admin user seed script and refactor common seed utilities" 2025-05-11 10:34:48 +08:00
louiscklaw
c29ab4b920 update project, 2025-05-11 08:02:50 +08:00
louiscklaw
9d46de56c3 update util scripts, 2025-05-11 08:02:07 +08:00
louiscklaw
3f9d88e733 init req0018, family photo of frameworks, 2025-05-11 08:00:31 +08:00
louiscklaw
6e576919ab init docker cofig, 2025-05-11 07:59:35 +08:00
louiscklaw
9739583f43 update ide config, remove yaml and python, 2025-05-11 07:59:11 +08:00
louiscklaw
bc1ec72df1 Update multiple markdown files with project guidelines, tasks, and database handling instructions 2025-05-11 07:57:34 +08:00
louiscklaw
e62dc5f597 "fix QuestionProgress component missing key props in button arrays," 2025-05-11 07:55:37 +08:00
louiscklaw
b5e9c8ba34 "update user popover with dynamic user metadata loading and improved UI consistency," 2025-05-11 07:55:16 +08:00
louiscklaw
9a8fd1c073 update auth guard and sign-in forms, add guidelines for custom auth to handle login and logout, 2025-05-11 07:54:23 +08:00
louiscklaw
25c1d3c917 update remove migration files in pocketbase, 2025-05-11 07:52:04 +08:00
louiscklaw
f435300740 update requirements, 2025-05-09 14:14:25 +08:00
louiscklaw
299567dd7c update requirements, 2025-05-09 14:14:00 +08:00
louiscklaw
fe1740d76f update user-meta, 2025-05-08 19:44:07 +08:00
louiscklaw
f840f02daf init draft from teacher collections, 2025-05-08 17:30:49 +08:00
242 changed files with 12512 additions and 3938 deletions

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=lf

5
.gitignore vendored
View File

@@ -2,8 +2,13 @@
node_modules
005_references/
_archive/
_del
*.bak
*.log
*.del
**/_del
**/volumes/**
006_lab
**/*.draft

12
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"recommendations": [
"aflalo.dbml-formatter",
"bierner.markdown-mermaid",
"christian-kohler.path-intellisense",
"esbenp.prettier-vscode",
"humao.rest-client",
"matt-meyers.vscode-dbml",
"nicolas-liger.dbml-viewer",
"yzhang.markdown-all-in-one"
]
}

21
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,21 @@
{
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"workbench.iconTheme": "material-icon-theme",
"workbench.colorTheme": "Default Dark Modern",
"editor.formatOnSave": true,
"git.ignoreLimitWarning": true
}

View File

@@ -0,0 +1,25 @@
this `tsx` file is clone from elsewhere, please understand, modify and update the content of `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/teachers/view/[id]/page.tsx.draft` to handle `Teacher` record thanks, modify comments/variables/paths/functions name please
---
please review and update all tsx files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Users` to make it handle `user` record thanks
---
<!-- read and understand @/_AI_WORKSPACE/greetings/001_greetings.md -->
## clone source code from one type to another
please understand, modify and update the content of `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/teacher/_GUIDELINES.md` to handle `Teacher` record thanks,
modify comments/variables/paths/functions name please
---
please help to update the tsx files inside folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/student` to handle the `student` record
## steps
- list all `tsx` files inside directory, remember the list
- clone the original `<original>.tsx` files to `<original>.tsx.draft`
- do all your modification within `<original>.tsx.draft` files, leave `original.tsx` unchange
---

View File

@@ -0,0 +1,33 @@
Hi, i need your help.
## task
i am working on a `dbml` file
i got a `schema.json` which is exported from pocketbase
and i want to update it to my current `dbml` file (one way process for documentation usage)
## Rules
- the collection from `json` file started with `_` can be ignored. they are system collection and should not appear in `dbml`
- one collection from `json` file mapped with one table in `dbml` file
- the `presentable` field from `json` file should be ignored.
- the `id` of collection in `json` file should be jod down in the comment of `dbml` file as an reference.
- you can find the comments in `schema.dbml` contains `pb_xxx` and that is the reference to the table in `schema.json` file.
## steps
- list the collection
## information
json file: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.json`
dbml file: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml`
## FAQ
1. 对于json中有但dbml中没有的表应该如何处理 添加为新表
1. 是否需要保留dbml文件中现有的注释和关系定义 完全保留
1. 字段类型映射是否有特殊规则? 沒有
1. please keep the existing comment
thanks

View File

@@ -0,0 +1,11 @@
# Greetings
Hi,
Imaging you are a software engineer and i will send you the guideline.
plesae read it, prepare yourself and i will tell you the task afterwards
please read and understand the markdown files in directory
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/000_AI_WORKSPACE/software_engineer/greetings`,
it provides background information of project i want you to help.
thanks

View File

@@ -0,0 +1,32 @@
# guideline
## principles
- at any time, please keep your answer, solution, explaination simple and short (K.I.S.S. or 大道至簡)
- please divide the problem into small parts
- if you found youself cannot understand the problem, please stop and ask how to do
- if you found youself cannot solve the problem, plesae stop and ask how to do
- review the whole solution before you reply to user
- if code syntax is already there, do follow (e.g. naming convention, syntax) the existing code
- no need to explain the reason until you are told to do so
- no need to show me the code change, at the end just simple summary in point form is ok
## highlighted project directories and their meanings
- `_ignore_this_directory` please ignore this directory and any files inside it
- `001_documentation` documentation of this project
- `002_source` source code of this project
- `002_source/cms` home of Context management system of this project
- use singular form for `src/components/dashboard` (e.g. `src/components/dashboard/student`)
- use plural form for `src/app/dashboard` (e.g. `src/app/dashboard/students`)
- `002_source/ionic_mobile` home of mobile client of this project
- `002_source/pocketbase` home of pocketbase home directory this project
- `003_test` e2e test of this project (not yet implemented)
- `004_marketing` marketing page of this project (not yet implemented)
- `005_references` opensource refence of this project
- `006_lab` my test (POC) of this project
- `README.md` Readme of this project
- `TODO.md` todo list of this project
- if the directory contains `_GUIDELINES.md`, please read it before operation

View File

@@ -0,0 +1,12 @@
# Knowledgebase
you can answer the question with below knowledge:
## frameworks and stacks
- if code syntax is already there, do follow (e.g. naming convention, syntax) the existing code
- make use of MCP `Context7` when you troubleshoot the problem with below topics:
- [pocketbase javascript SDK](https://context7.com/pocketbase/js-sdk/llms.txt)
- [DBML](https://context7.com/holistics/dbml/llms.txt)
- [ionic framework](https://context7.com/ionic-team/ionic-framework/llms.txt)
- [nextjs 14 app router](https://context7.com/nextjsargentina/next.js-docs/llms.txt)

View File

@@ -0,0 +1,22 @@
# FAQ
Q: where is `dbml` file ?
A: dbml file located in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml`
Q: when file not found, do i need to search it in `_ignore_this_directory` ?
A: No, you just stop there and voice out.
Q: Shall I assume the component is already exist ?
A: yes, you can assume that
Q: Is `COL_USER_METAS` the collection for User related (e.g. `Teacher`, `Student`) collections?
A: yes
Q: Shall I verify `import` or `types` when do modification job ?
A: No, you just replace the name of the function, variables etc is ok. no need to check for dependencies thanks.
Q: how to list files with `.tsx.draft` extensions in `src/db/UserMetas` folder?
A: using command like `find src/db/UserMetas -name "*.tsx.draft" -type f -ls` to list the files with `.tsx.draft` extendions only exist in `src/db/UserMetas`
Q: when user want to modify `.tsx.draft` file, do i need to take care the `.tsx` file as well?
A: No, no don't need to, user will handle the remaining modifications. please restrict your modification in the mentioned file or directory only.

View File

@@ -0,0 +1,39 @@
# database and schemas
## getting started
Imagine there is a:
1. developer (provide the modification)
2. QA engineer (provide the feedback, and testing)
3. software engineer
4. technical writer
they will:
- conclude and integrate the ideas from developer and QA engineer
- make decision to modify the code accordingly.
## project background and initial setup
- **IMPORTANT**: No need to reply me what you are going on and your digest in this phase.
No need to show me your code plan
Just reply me "OK" when done
- base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
- `schema.dbml`
- read `<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
- look into the md files in folder `<base_dir>/002_source/ionic_mobile/_AI_WORKSPACE/001_guideline`
- if the directory user provided contins `_GUIDELINES.md`, please read the file
- read the files, remember and link up the ideas in file stated above, i will tell them the task afterwards
- please review at least 3 times after you modified the code

View File

@@ -0,0 +1,21 @@
please review and update all tsx files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Users` to make it handle `user` record thanks
---
<!-- read and understand @/_AI_WORKSPACE/greetings/001_greetings.md -->
## clone source code from one type to another
please understand, modify and update the content of `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/teacher/_GUIDELINES.md` to handle `Teacher` record thanks,
modify comments/variables/paths/functions name please
---
please help to update the tsx files inside folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/student` to handle the `student` record
## steps
- list all `tsx` files inside directory, remember the list
- clone the original `<original>.tsx` files to `<original>.tsx.draft`
- do all your modification within `<original>.tsx.draft` files, leave `original.tsx` unchange
---

View File

@@ -0,0 +1,33 @@
Hi, i need your help.
## task
i am working on a `dbml` file
i got a `schema.json` which is exported from pocketbase
and i want to update it to my current `dbml` file (one way process for documentation usage)
## Rules
- the collection from `json` file started with `_` can be ignored. they are system collection and should not appear in `dbml`
- one collection from `json` file mapped with one table in `dbml` file
- the `presentable` field from `json` file should be ignored.
- the `id` of collection in `json` file should be jod down in the comment of `dbml` file as an reference.
- you can find the comments in `schema.dbml` contains `pb_xxx` and that is the reference to the table in `schema.json` file.
## steps
- list the collection
## information
json file: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.json`
dbml file: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml`
## FAQ
1. 对于json中有但dbml中没有的表应该如何处理 添加为新表
1. 是否需要保留dbml文件中现有的注释和关系定义 完全保留
1. 字段类型映射是否有特殊规则? 沒有
1. please keep the existing comment
thanks

View File

@@ -0,0 +1,8 @@
Hi, i will send you the guideline,
plesae read it, prepare yourself and i will tell you the task afterwards
please read and understand the markdown files in directory
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/000_AI_WORKSPACE/_AI_WORKSPACE/greetings`,
it provides background information of project i want you to help.
thanks

View File

@@ -0,0 +1,28 @@
# guideline
## principles
- at any time, please keep your answer, solution, explaination simple and short (K.I.S.S. or 大道至簡)
- please divide the problem into small parts
- if you found youself cannot understand the problem, please stop and ask how to do
- if you found youself cannot solve the problem, plesae stop and ask how to do
- review the whole solution before you reply to user
- if code syntax is already there, do follow (e.g. naming convention, syntax) the existing code
- no need to explain the reason until you are told to do so
- no need to show me the code change, at the end just simple summary in point form is ok
## highlighted project directories and their meanings
- `_ignore_this_directory` please ignore this directory and any files inside it
- `001_documentation` documentation of this project
- `002_source` source code of this project
- `002_source/cms` home of Context management system of this project
- `002_source/ionic_mobile` home of mobile client of this project
- `002_source/pocketbase` home of pocketbase home directory this project
- `003_test` e2e test of this project (not yet implemented)
- `004_marketing` marketing page of this project (not yet implemented)
- `005_references` opensource refence of this project
- `006_lab` my test (POC) of this project
- `README.md` Readme of this project
- `TODO.md` todo list of this project

View File

@@ -0,0 +1,12 @@
# Knowledgebase
you can answer the question with below knowledge:
## frameworks and stacks
- if code syntax is already there, do follow (e.g. naming convention, syntax) the existing code
- make use of MCP `Context7` when you troubleshoot the problem with below topics:
- [pocketbase javascript SDK](https://context7.com/pocketbase/js-sdk/llms.txt)
- [DBML](https://context7.com/holistics/dbml/llms.txt)
- [ionic framework](https://context7.com/ionic-team/ionic-framework/llms.txt)
- [nextjs 14 app router](https://context7.com/nextjsargentina/next.js-docs/llms.txt)

View File

@@ -0,0 +1,7 @@
# FAQ
Q: where is `dbml` file ?
A: dbml file located in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml`
Q: when file not found, do i need to search it in `_ignore_this_directory` ?
A: No, you just stop there and voice out.

View File

@@ -0,0 +1,48 @@
# database and schemas
## getting started
Imagine there is a:
1. developer (provide the modification)
2. QA engineer (provide the feedback, and testing)
3. software engineer
4. technical writer
they will:
- conclude and integrate the ideas from developer and QA engineer
- make decision to modify the code accordingly.
## project background and initial setup
- **IMPORTANT**: No need to reply me what you are going on and your digest in this phase.
No need to show me your code plan
Just reply me "OK" when done
- base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
- `schema.dbml`
- read `<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
- look into the md files in folder `<base_dir>/002_source/ionic_mobile/_AI_WORKSPACE/001_guideline`
- if the directory user provided contins `_GUIDELINES.md`, please read the file
- read the files, remember and link up the ideas in file stated above, i will tell them the task afterwards
- please review at least 3 times after you modified the code
## frameworks documentation and samples
- react
- ionic and capacitor
- pocketbase
- tanstack/react-query
- vite
- typescript

View File

@@ -1,6 +1,5 @@
{
"recommendations": [
"redhat.vscode-yaml",
"yzhang.markdown-all-in-one",
"esbenp.prettier-vscode",
"ms-python.python",
@@ -8,7 +7,6 @@
"ms-python.debugpy",
"ms-python.black-formatter",
"ms-python.isort",
"ms-python.pylint",
"bierner.markdown-mermaid",
"shd101wyy.markdown-preview-enhanced",
"yzhang.markdown-all-in-one",

View File

@@ -24,5 +24,4 @@ mindmap
Tools
Pen and paper
Mermaid
```

View File

@@ -1,9 +1,27 @@
---
tags: mobile, lesson
tags: time
---
# lesson page documentation
# time tracking
it should have a lesson page
T.B.A.
![alt text](image.png)
```mermaid
gantt
title A Gantt Diagram
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :after a1, 20d
section Another
Task in Another :2014-01-12, 12d
another task :24d
```
---
## TODOs of the project
- [cms TODO](../../../002_source/cms/TODO.md)
- [ionic TODO](../../../002_source/ionic_mobile/TODO.md)
- [pocketbase TODO](../../../002_source/pocketbase/TODO.md)

View File

@@ -0,0 +1,20 @@
# task
update `dbml` from `schema.json`, for collection `billingAddress`
## Background information
1. please ignore `presentable` properties from `schema.json`, this is for pocketbase internal usage
1. please ignore collections with `_` as its first character, this is for pocketbase internal usage
1. just return the collection with `billingAddress` is ok, please ignore other collections
## steps
1. read file `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/schema.json`. this is the export from `pocketbase`.
1. read file `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml`. this is file written in dbml format.
1. currently the collection in `schama.json` is mapped to table in `schema.dbml`
1. compare the `schema.json` and `schema.dbml`, remember the differences
1. you may found some comment already exist in `schema.dbml`, please keep them
1. while keeping `schema.json` content unchanged. write file to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema_ai_draft.dbml` content based on `schema.json`.
thanks.

View File

@@ -1,20 +0,0 @@
# LessonCategories / LessonCategory
Hi, using information from
- @schema.dbml for schemas, fields and table names,
- @listHelloworld.ts for skeleton for ts script
- @listVocabularies.ts, @listUserMetas.ts, @listQuizListening.ts, @listQuisMatching.ts, @listQuizConnectivesCategories for reference
extend @listLessonCategories.ts to cover `LessonCategories`,
draft `ts` code
- `CRUD`
- `getFulllistLessonCategories,`
- `getListByFilterLessonCategories,`
- `getFirstListItemLessonCategories,`
- `getOneLessonCategory,`
- `getListLessonCategoryById`
thanks

View File

@@ -1,20 +0,0 @@
# LessonTypes / LessonType
Hi, using information from
- @schema.dbml for schemas, fields and table names,
- @listHelloworld.ts for skeleton for ts script
- @listVocabularies.ts, @listUserMetas.ts, @listQuizListening.ts, @listQuisMatching.ts, @listQuizConnectivesCategories for reference
extend @listLessonTypes.ts to cover `LessonTypes`,
draft `ts` code
- `CRUD`
- `getFulllistLessonTypes,`
- `getListByFilterLessonTypes,`
- `getFirstListItemLessonTypes,`
- `getOneLessonType,`
- `getListLessonTypeById`
thanks

View File

@@ -1,20 +0,0 @@
# QuizCategories / QuizCategory
Hi, using information from
- @schema.dbml for schemas, fields and table names,
- @listHelloworld.ts for skeleton for ts script
- @listVocabularies.ts, @listUserMetas.ts, @listQuizListening.ts, @listQuisMatching.ts, @listQuizConnectivesCategories for reference
extend @listQuizCategories.ts to cover `QuizCategories`,
draft `ts` code
- `CRUD`
- `getFulllistQuizCategories,`
- `getListByFilterQuizCategories,`
- `getFirstListItemQuizCategories,`
- `getOneQuizCategory,`
- `getListQuizCategoryById`
thanks

View File

@@ -1,20 +0,0 @@
# QuizConnectives / QuizConnective
Hi, using information from
- @schema.dbml for schemas, fields and table names,
- @listHelloworld.ts for skeleton for ts script
- @listVocabularies.ts, @listUserMetas.ts, @listQuizListening.ts, @listQuisMatching.ts, @listQuizConnectivesCategories for reference
extend @listQuizConnectives.ts to cover `QuizConnectives`,
draft `ts` code
- `CRUD`
- `getFulllistQuizConnectives,`
- `getListByFilterQuizConnectives,`
- `getFirstListItemQuizConnectives,`
- `getOneQuizConnective,`
- `getListQuizConnectiveById`
thanks

View File

@@ -1,20 +0,0 @@
# QuizConnectivesCategories / QuizConnectivesCategory
Hi, using information from
- @schema.dbml for schemas, fields and table names,
- @listHelloworld.ts for skeleton for ts script
- @listVocabularies.ts, @listUserMetas.ts, @listQuizListening.ts, @listQuisMatching.ts, @listQuizConnectivesCategories for reference
extend @listQuizConnectivesCategories.ts to cover `QuizConnectivesCategories`,
draft `ts` code
- `CRUD`
- `getFulllistQuizConnectivesCategories,`
- `getListByFilterQuizConnectivesCategories,`
- `getFirstListItemQuizConnectivesCategories,`
- `getOneQuizConnectivesCategory,`
- `getListQuizConnectivesCategoryById`
thanks

View File

@@ -1,20 +0,0 @@
# QuizListenings / QuizListening
Hi, using information from
- @schema.dbml for schemas, fields and table names,
- @listHelloworld.ts for skeleton for ts script
- @listVocabularies.ts, @listUserMetas.ts, @listQuizListening.ts, @listQuisMatching.ts, @listQuizConnectivesCategories for reference
extend @listQuizListenings.ts to cover `QuizListenings`,
draft `ts` code
- `CRUD`
- `getFulllistQuizListenings,`
- `getListByFilterQuizListenings,`
- `getFirstListItemQuizListenings,`
- `getOneQuizListening,`
- `getListQuizListeningById`
thanks

View File

@@ -1,20 +0,0 @@
# QuizMatchings / QuizMatching
Hi, using information from
- @schema.dbml for schemas, fields and table names,
- @listHelloworld.ts for skeleton for ts script
- @listVocabularies.ts, @listUserMetas.ts, @listQuizListening.ts, @listQuisMatching.ts, @listQuizConnectivesCategories for reference
extend @listQuizMatchings.ts to cover `QuizMatchings`,
draft `ts` code
- `CRUD`
- `getFulllistQuizMatchings,`
- `getListByFilterQuizMatchings,`
- `getFirstListItemQuizMatchings,`
- `getOneQuizMatching,`
- `getListQuizMatchingById`
thanks

View File

@@ -1,20 +0,0 @@
# UserMetas / UserMeta
Hi, using information from
- @schema.dbml for schemas, fields and table names,
- @listHelloworld.ts for skeleton for ts script
- @listVocabularies.ts, @listUserMetas.ts, @listQuizListening.ts, @listQuisMatching.ts, @listQuizConnectivesCategories for reference
extend @listUserMetas.ts to cover `UserMetas`,
draft `ts` code
- `CRUD`
- `getFulllistUserMetas,`
- `getListByFilterUserMetas,`
- `getFirstListItemUserMetas,`
- `getOneUserMeta,`
- `getListUserMetaById`
thanks

View File

@@ -1,20 +0,0 @@
# Users / User
Hi, using information from
- @schema.dbml for schemas, fields and table names,
- @listHelloworld.ts for skeleton for ts script
- @listVocabularies.ts, @listUserMetas.ts, @listQuizListening.ts, @listQuisMatching.ts, @listQuizConnectivesCategories for reference
extend @listUsers.ts to cover `Users`,
draft `ts` code
- `CRUD`
- `getFulllistUsers,`
- `getListByFilterUsers,`
- `getFirstListItemUsers,`
- `getOneUser,`
- `getListUserById`
thanks

View File

@@ -1,20 +0,0 @@
# Vocabularies / Vocabulary
Hi, using information from
- @schema.dbml for schemas, fields and table names,
- @listHelloworld.ts for skeleton for ts script
- @listVocabularies.ts, @listUserMetas.ts, @listQuizListening.ts, @listQuisMatching.ts, @listQuizConnectivesCategories for reference
extend @listVocabularies.ts to cover `Vocabularies`,
draft `ts` code
- `CRUD`
- `getFulllistVocabularies,`
- `getListByFilterVocabularies,`
- `getFirstListItemVocabularies,`
- `getOneVocabulary,`
- `getListVocabularyById`
thanks

View File

@@ -0,0 +1,6 @@
# GUIDELINES
- `schema.dbml` means DB schema in `dbml` format
- `schema.json` contains the exported DB schema from pocketbase
- the `Collection ID: xxx` in `dbml` file contains the collection `id` mapping from `schema.json`
- the `presentable` field from `json` file should be ignored.

View File

@@ -0,0 +1,86 @@
//
// # REQ0015:
// ## PURPOSE:
//
// This is a script to convert `schema.json` exported from pocketbase to dbml formatted file `schema.dbml`
//
//
const fs = require("fs");
const path = require("path");
// Load schema.json
const schemaPath = path.join(process.cwd(), "schema.json");
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
// Type mapping from PocketBase to DBML/SQL
const typeMapping = {
text: "text",
number: "integer",
bool: "boolean",
email: "text",
url: "varchar",
date: "datetime",
select: "varchar",
json: "text",
file: "file",
password: "varchar",
relation: "integer", // Assuming relations are stored as integer IDs
autodate: "datetime",
};
function convertToDBML(schema) {
let dbml = "";
dbml = "// Generated at: " + new Date().toISOString();
dbml = dbml + "\n\n\n";
let collectionIdToTableMapping = {};
for (const [idx, tableDef] of Object.entries(schema)) {
collectionIdToTableMapping[tableDef.id] = tableDef.name;
}
for (const [idx, tableDef] of Object.entries(schema)) {
console.log({ idx });
dbml += `// \n`;
dbml += `// collection id: ${tableDef.id} \n`;
dbml += `// collection name: ${tableDef.name} \n`;
dbml += `// collection type: ${tableDef.type} \n`;
dbml += `Table ${tableDef.name} {\n`;
for (const field of tableDef.fields) {
// Get field type
let fieldType = typeMapping[field.type] || "text";
// Handle special cases
if (field.type === "number" && field.options?.max === 1) {
fieldType = "boolean";
}
// Build field definition
let line = ` ${field.name} ${fieldType}`;
// Add constraints
const constraints = [];
if (field.name === "id" || field.primary) constraints.push("pk");
if (field.required) constraints.push("not null");
if (constraints.length > 0) line += ` [${constraints.join(", ")}]`;
if (field.collectionId) {
line += ` [ref: > ${
collectionIdToTableMapping[field.collectionId]
}.id] // ${field.id}`;
}
dbml += `${line}\n`;
console.log({ line });
}
dbml += "}\n\n";
}
return dbml;
}
// Convert and save
const dbmlOutput = convertToDBML(schema);
fs.writeFileSync("schema.dbml", dbmlOutput);
console.log("DBML file generated successfully!");

View File

@@ -0,0 +1,31 @@
import { faker } from "@faker-js/faker";
const getId = (id) => id.padStart(15, "0");
const row_array = Array.from({ length: 10 }, (_, i) => [
getId(String(i + 1)),
faker.person.firstName(),
"",
faker.internet.email(),
faker.phone.number(),
faker.company.name(),
{
country: faker.location.country(),
state: faker.location.state(),
city: faker.location.city(),
zipCode: faker.location.zipCode(),
line1: faker.location.streetAddress(),
line2: faker.location.secondaryAddress(),
},
Math.floor(Math.random() * (100 - 0 + 1)) + 0,
faker.location.timeZone(),
["en", "de", "es", "fr", "ja", "ko", "zh-CN"].sort(
() => Math.random() - 0.5
)[0],
faker.finance.currencyCode(),
]);
import fs from "fs";
const filePath = "output.json";
fs.writeFileSync(filePath, JSON.stringify(row_array, null, 2));
console.log(`Wrote ${row_array.length} records to ${filePath}`);

View File

@@ -0,0 +1,202 @@
[
[
"000000000000001",
"May",
"",
"June_Wintheiser33@hotmail.com",
"281-378-5900 x822",
"Green, Rempel and Hoeger",
{
"country": "Central African Republic",
"state": "Arizona",
"city": "Winfieldburgh",
"zipCode": "92017-8004",
"line1": "1838 Willa Freeway",
"line2": "Suite 307"
},
98,
"Asia/Urumqi",
"de",
"MUR"
],
[
"000000000000002",
"Marilyne",
"",
"Carol_Blick@yahoo.com",
"(893) 919-2445 x193",
"White - Hessel",
{
"country": "Iraq",
"state": "Nevada",
"city": "Casa Grande",
"zipCode": "83831-3843",
"line1": "6984 Alberto Radial",
"line2": "Suite 154"
},
49,
"Africa/Tunis",
"zh-CN",
"CHF"
],
[
"000000000000003",
"Jacklyn",
"",
"Tamara_Lynch11@yahoo.com",
"597-593-0144 x168",
"Rolfson LLC",
{
"country": "Grenada",
"state": "Georgia",
"city": "New Brodyfort",
"zipCode": "18887-7075",
"line1": "493 Pfannerstill Meadow",
"line2": "Apt. 358"
},
44,
"Asia/Manila",
"zh-CN",
"CDF"
],
[
"000000000000004",
"Alana",
"",
"Ahmed_Willms@hotmail.com",
"401.212.0386 x31125",
"Friesen, Langworth and Thompson",
{
"country": "Australia",
"state": "North Carolina",
"city": "Fort Jerrell",
"zipCode": "14211",
"line1": "1763 West Street",
"line2": "Suite 699"
},
34,
"America/Boa_Vista",
"ja",
"KES"
],
[
"000000000000005",
"Rocky",
"",
"Angela_Kuhic@gmail.com",
"653.964.0412",
"Hayes - Morar",
{
"country": "Reunion",
"state": "New York",
"city": "Kayton",
"zipCode": "82048-0645",
"line1": "636 Angel Junction",
"line2": "Apt. 361"
},
70,
"America/Grand_Turk",
"fr",
"PGK"
],
[
"000000000000006",
"Carmela",
"",
"Larry94@hotmail.com",
"1-508-980-1889 x134",
"Goodwin - Brown",
{
"country": "Heard Island and McDonald Islands",
"state": "Wisconsin",
"city": "Jalenbury",
"zipCode": "75732-7013",
"line1": "669 Sven Trail",
"line2": "Suite 409"
},
48,
"Asia/Karachi",
"fr",
"AMD"
],
[
"000000000000007",
"Lucious",
"",
"Lera67@yahoo.com",
"(684) 748-3653 x8745",
"Harvey - Kilback",
{
"country": "Israel",
"state": "Maryland",
"city": "East Allenmouth",
"zipCode": "21779",
"line1": "6070 W Grand Avenue",
"line2": "Suite 448"
},
83,
"America/St_Barthelemy",
"en",
"SBD"
],
[
"000000000000008",
"Anika",
"",
"Shane.Moore@gmail.com",
"(996) 909-3575 x614",
"Donnelly, Larson and Stamm",
{
"country": "Canada",
"state": "Michigan",
"city": "Lafayette",
"zipCode": "90430-8775",
"line1": "430 Orland Place",
"line2": "Suite 891"
},
64,
"America/Toronto",
"fr",
"PKR"
],
[
"000000000000009",
"Otto",
"",
"Emery_Wisozk@yahoo.com",
"1-602-251-7035 x9461",
"Kreiger - Boehm",
{
"country": "South Georgia and the South Sandwich Islands",
"state": "Colorado",
"city": "Lake Isaias",
"zipCode": "26025-5909",
"line1": "143 Kautzer Unions",
"line2": "Apt. 752"
},
98,
"Africa/Ndjamena",
"fr",
"JOD"
],
[
"000000000000010",
"Cortez",
"",
"Jameson13@hotmail.com",
"1-660-472-1494",
"Parisian LLC",
{
"country": "Mali",
"state": "Illinois",
"city": "Stammburgh",
"zipCode": "92318",
"line1": "7669 Jude Drive",
"line2": "Apt. 594"
},
59,
"Africa/Lubumbashi",
"en",
"VND"
]
]

View File

@@ -0,0 +1,33 @@
{
"name": "gen_customer",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gen_customer",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@faker-js/faker": "^9.7.0"
}
},
"node_modules/@faker-js/faker": {
"version": "9.7.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.7.0.tgz",
"integrity": "sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/fakerjs"
}
],
"license": "MIT",
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
}
}
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "gen_customer",
"version": "1.0.0",
"main": "gen_customer.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@faker-js/faker": "^9.7.0"
}
}

View File

@@ -0,0 +1,23 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@faker-js/faker':
specifier: ^9.7.0
version: 9.7.0
packages:
'@faker-js/faker@9.7.0':
resolution: {integrity: sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg==}
engines: {node: '>=18.0.0', npm: '>=9.0.0'}
snapshots:
'@faker-js/faker@9.7.0': {}

View File

@@ -1,266 +1,465 @@
// Users table with auth fields
Table Users {
// system field
id text [pk]
tokenKey text [not null]
created datetime [default: `now()`]
updated datetime
password text [not null]
// Generated at: 2025-05-08T05:00:49.862Z
// value field
//
// collection id: pbc_3142635823
// collection name: _superusers
// collection type: auth
Table _superusers {
id text [pk, not null]
password varchar [not null]
tokenKey text [not null]
email text [not null]
emailVisibility boolean
verified boolean
created datetime
updated datetime
}
//
// collection id: _pb_users_auth_
// collection name: users
// collection type: auth
Table users {
id text [pk, not null]
password varchar [not null]
tokenKey text [not null]
email text [not null]
emailVisibility boolean
verified boolean
name text
avatar file
created datetime
updated datetime
visible text
}
// LessonTypes stores different types of lessons
// lesson_types, lesson_type
Table LessonTypes {
// system field
id text [pk] // changed from int to text
created datetime [default: `now()`] // timestamp when the lesson type was created
updated datetime // timestamp when the lesson type was last updated
// value field
name text // changed from varchar to text
type text // changed from varchar to text
//
// collection id: pbc_1430376151
// collection name: Categories
// collection type: base
Table Categories {
id text [pk, not null]
cat_name text
cat_image_url text
cat_image file
pos integer
lesson_id integer [ref: > LessonsTypes.id] // relation3455582614
remarks text
visible text
created datetime
updated datetime
}
//
// collection id: pbc_108570809
// collection name: Customers
// collection type: base
Table Customers {
id text [pk, not null]
name text
email text
phone text
quota integer
status text
avatar_file file
user_id integer [ref: > users.id] // relation2809058197
billingAddress text
timezone text
language text
currency text
created datetime
updated datetime
}
//
// collection id: pbc_1196309394
// collection name: LessonsCategories
// collection type: base
Table LessonsCategories {
id text [pk, not null]
cat_name text
cat_image_url text
cat_image file
pos integer
lesson_id integer [ref: > LessonsTypes.id] // relation3455582614
description text
remarks text
visible text
created datetime
updated datetime
}
//
// collection id: pbc_2328411368
// collection name: LessonsTypes
// collection type: base
Table LessonsTypes {
id text [pk, not null]
name text
type text
pos integer
visible text
field date
}
// LessonCategories stores categories of lessons
// lesson_categories, lesson_category
Table LessonCategories {
// system field
id text [pk] // changed from int to text
created datetime [default: `now()`] // timestamp when the category was created
updated datetime // timestamp when the category was last updated
// value field
cat_name text // changed from varchar to text
cat_image_url text // new field
cat_image file // changed from varchar to file
pos integer
lesson_id integer [ref: > LessonTypes.id] // foreign key referencing LessonTypes.id
description text // new field
remarks text // changed from varchar to text
visible text // new field
}
Table Helloworlds {
// system field
id text [pk] // changed from int to text
created datetime [default: `now()`] // record create time
updated datetime // record update time
hello text // new field
}
Table UserMetas {
// system field
id text [pk] // changed from int to text
created datetime [default: `now()`]
created datetime
updated datetime
// value field
helloworld text // changed from varchar to text
meta json // new field
user_id text [ref: > Users.id] // changed type and reference
state text // new field
avatar file // changed from blob to file
role text // new field
field datetime
}
Table QuizCategories {
// system field
id text [pk] // changed from int to text
created datetime [default: `now()`]
//
// collection id: pbc_977978967
// collection name: Notifications
// collection type: base
Table Notifications {
id text [pk, not null]
read boolean
type text
author text
job text
description text
NOTI_ID text
created datetime
updated datetime
// value field
cat_name text // changed from varchar to text
cat_image text // changed from varchar to text
init_answer json // new field
}
// stores all questions of matching frenzy
Table QuizMatchings {
// system field
id text [pk] // changed from int to text
created datetime [default: `now()`] // record create time
updated datetime // record update time
// value field
word text // changed from varchar to text
word_c text // changed from varchar to text
cat_id text [ref: > QuizCategories.id] // changed type and reference
}
// QuizListening stores all listening quiz data
Table QuizListenings {
// system field
id text [pk] // changed from int to text
created datetime [default: `now()`] // record create time
updated datetime // record update time
// value field
sound file // changed from varchar to file
word text // changed from varchar to text
cat_id text [ref: > QuizCategories.id] // changed type and reference
}
// stores all categories of connectives revision quiz
Table QuizConnectivesCategories {
// system field
id text [pk] // changed from int to text
created datetime [default: `now()`] // record create time
updated datetime // record update time
// value field
cat_name text // changed from varchar to text
cat_image file // changed from varchar to file
}
// stores all questions of connectives revision quiz
Table QuizConnectives {
// system field
id text [pk] // changed from int to text
created datetime [default: `now()`] // record create time
updated datetime // record update time
// value field
question_fh text // changed from varchar to text
question_sh text // changed from varchar to text
modal_ans text // changed from varchar to text
cat_id text [ref: > QuizConnectivesCategories.id] // changed type and reference
}
// Lessons stores all lessons in the database
Table Vocabularies {
// system field
id text [pk] // changed from int to text
created datetime [default: `now()`] // timestamp when the lesson was created
updated datetime // timestamp when the lesson was last updated
// value field
image file // changed from varchar to file
sound file // changed from varchar to file
word text // changed from varchar to text
word_c text // changed from varchar to text
sample_e text // changed from varchar to text
sample_c text // changed from varchar to text
cat_id text [ref: > LessonCategories.id] // changed type and reference
category text // changed from varchar to text
lesson_type_id text [ref: > LessonTypes.id] // changed type and reference
}
// Listening Practice Quiz Categories
// store listening practice category, (LpCategories, LpCategory)
Table QuizLPCategories {
// system fields
id text [pk]
created datetime [default: `now()`]
updated datetime
// value fields
//
// collection id: pbc_4061499106
// collection name: QuizCRCategories
// collection type: base
Table QuizCRCategories {
id text [pk, not null]
cat_name text
cat_image file
pos number
init_answer json
pos integer
init_answer text
created datetime
updated datetime
}
//
// collection id: pbc_3141885671
// collection name: QuizCRQuestions
// collection type: base
Table QuizCRQuestions {
id text [pk, not null]
question_fh text
question_sh text
modal_ans text
cat_id integer [ref: > QuizCRCategories.id] // relation1827623476
options text
created datetime
updated datetime
init_answer text
}
//
// collection id: pbc_3571292172
// collection name: QuizCategories
// collection type: base
Table QuizCategories {
id text [pk, not null]
cat_name text
cat_image text
init_answer text
created datetime
updated datetime
}
//
// collection id: pbc_96745150
// collection name: QuizConnectives
// collection type: base
Table QuizConnectives {
id text [pk, not null]
question_fh text
question_sh text
modal_ans text
cat_id integer [ref: > QuizConnectivesCategories.id] // relation3870140739
created datetime
updated datetime
}
//
// collection id: pbc_342761728
// collection name: QuizConnectivesCategories
// collection type: base
Table QuizConnectivesCategories {
id text [pk, not null]
cat_name text
cat_image file
created datetime
updated datetime
}
//
// collection id: pbc_3639453778
// collection name: QuizLPCategories
// collection type: base
Table QuizLPCategories {
id text [pk, not null]
cat_name text
cat_image file
pos integer
init_answer text
created datetime
updated datetime
visible text
slug text
remarks text
description text
}
// Listening Practice Quiz Questions
//
// collection id: pbc_742947356
// collection name: QuizLPQuestions
// collection type: base
Table QuizLPQuestions {
id text [pk] // changed from int to text
created datetime [default: `now()`]
id text [pk, not null]
word text
sound file
cat_id integer [ref: > QuizLPCategories.id] // relation3870140739
created datetime
updated datetime
word text // changed from varchar to text
sound file // changed from blob to file
cat_id text [ref: > QuizLPCategories.id] // changed type and reference
cat_name text // new field
cat_image file // new field
pos number // new field
init_answer json // new field
visible text // new field
slug text // new field
remarks text // new field
description text // new field
}
// Matching Frenzy Quiz Categories
Table QuizMFCategories {
id text [pk] // changed from int to text
created datetime [default: `now()`]
updated datetime
cat_name text // changed from varchar to text
cat_image file // changed from blob to file
pos number // changed from integer to number
init_answer json
visible text // new field
}
// Matching Frenzy Quiz Questions
Table QuizMFQuestions {
id text [pk] // changed from int to text
created datetime [default: `now()`]
updated datetime
word text // changed from varchar to text
word_c text // changed from varchar to text
cat_id text [ref: > QuizMFCategories.id] // changed type and reference
visible text // new field
sound file // new field
cat_image file // new field
}
// Connectives Revision Quiz Categories
Table QuizCRCategories {
id text [pk] // changed from int to text
created datetime [default: `now()`]
updated datetime
cat_name text // changed from varchar to text
cat_image file // changed from blob to file
cat_name text
cat_image file
pos integer
init_answer json
init_answer text
visible text
slug text
remarks text
description text
}
// Connectives Revision Quiz Questions
Table QuizCRQuestions {
id text [pk] // changed from int to text
created datetime [default: `now()`]
//
// collection id: pbc_2511066072
// collection name: QuizListenings
// collection type: base
Table QuizListenings {
id text [pk, not null]
sound file
word text
cat_id integer [ref: > QuizCategories.id] // relation3870140739
created datetime
updated datetime
question_fh text // changed from varchar to text
question_sh text // changed from varchar to text
modal_ans text // changed from varchar to text
cat_id text [ref: > QuizCRCategories.id] // changed type and reference
options json // new field
}
// Test table
Table t1 {
id text [pk] // changed from int to text
created datetime [default: `now()`]
//
// collection id: pbc_84667061
// collection name: QuizMFCategories
// collection type: base
Table QuizMFCategories {
id text [pk, not null]
cat_name text
cat_image file
pos integer
init_answer text
created datetime
updated datetime
hello text // changed from name to hello
test_file file // new field
visible text
}
// Customers table
Table Customers {
id text [pk] // new table
created datetime [default: `now()`]
//
// collection id: pbc_3346420851
// collection name: QuizMFQuestions
// collection type: base
Table QuizMFQuestions {
id text [pk, not null]
word text
word_c text
cat_id integer [ref: > QuizMFCategories.id] // relation3870140739
created datetime
updated datetime
visible text
sound file
cat_image file
init_answer text
}
//
// collection id: pbc_2936646783
// collection name: QuizMatchings
// collection type: base
Table QuizMatchings {
id text [pk, not null]
word text
word_c text
cat_id integer [ref: > QuizCategories.id] // relation3870140739
created datetime
updated datetime
}
//
// collection id: pbc_491894781
// collection name: Students
// collection type: base
Table Students {
id text [pk, not null]
name text
email text
phone text
quota number
quota integer
status text
avatar_file file
cat_id text [ref: > QuizMFCategories.id] // refer to a single user in `Users` table
user_id integer [ref: > users.id] // relation2809058197
billingAddress text
timezone text
language text
currency text
created datetime
updated datetime
}
//
// collection id: pbc_1413424569
// collection name: Teachers
// collection type: base
Table Teachers {
id text [pk, not null]
name text
email text
phone text
quota integer
status text
avatar_file file
user_id integer [ref: > users.id] // relation2809058197
billingAddress text
timezone text
language text
currency text
created datetime
updated datetime
}
//
// collection id: pbc_1305841361
// collection name: UserMetas
// collection type: base
Table UserMetas {
id text [pk, not null]
helloworld text
meta text
user_id integer [ref: > users.id] // relation2809058197
created datetime
updated datetime
status text
avatar file
role text
name text
email text
phone text
avatar_file file
}
//
// collection id: pbc_1638686383
// collection name: Vocabularies
// collection type: base
Table Vocabularies {
id text [pk, not null]
image file
sound file
word text
word_c text
sample_e text
sample_c text
cat_id integer [ref: > Categories.id] // relation3870140739
category text
lesson_type_id integer [ref: > LessonsTypes.id] // relation808508980
created datetime
updated datetime
visible text
type text
}
//
// collection id: pbc_4275539003
// collection name: _authOrigins
// collection type: base
Table _authOrigins {
id text [pk, not null]
collectionRef text [not null]
recordRef text [not null]
fingerprint text [not null]
created datetime
updated datetime
}
//
// collection id: pbc_2281828961
// collection name: _externalAuths
// collection type: base
Table _externalAuths {
id text [pk, not null]
collectionRef text [not null]
recordRef text [not null]
provider text [not null]
providerId text [not null]
created datetime
updated datetime
}
//
// collection id: pbc_2279338944
// collection name: _mfas
// collection type: base
Table _mfas {
id text [pk, not null]
collectionRef text [not null]
recordRef text [not null]
method text [not null]
created datetime
updated datetime
}
//
// collection id: pbc_1638494021
// collection name: _otps
// collection type: base
Table _otps {
id text [pk, not null]
collectionRef text [not null]
recordRef text [not null]
password varchar [not null]
sentTo text
created datetime
updated datetime
}
//
// collection id: pbc_1509025625
// collection name: billingAddress
// collection type: base
Table billingAddress {
id text [pk, not null]
country text
state text
city text
zipCode text
line1 text
line2 text
created datetime
updated datetime
}
//
// collection id: pbc_123408445
// collection name: helloworlds
// collection type: base
Table helloworlds {
id text [pk, not null]
hello text
created datetime
updated datetime
}
//
// collection id: pbc_2109205374
// collection name: t1
// collection type: base
Table t1 {
id text [pk, not null]
hello text
created datetime
updated datetime
test_file file
}

View File

@@ -1,215 +0,0 @@
// LessonTypes stores different types of lessons
// lesson_types, lesson_type
Table LessonTypes {
// system field
id int [pk, increment] // unique identifier for the lesson type
created datetime [default: `now()`] // timestamp when the lesson type was created
updated datetime // timestamp when the lesson type was last updated
// value field
name varchar // name of the lesson type
type varchar // type category
}
// LessonCategories stores categories of lessons
// lesson_categories, lesson_category
Table LessonCategories {
// system field
id int [pk, increment] // unique identifier for the lesson category
created datetime [default: `now()`] // timestamp when the category was created
updated datetime // timestamp when the category was last updated
// value field
cat_name varchar // image file name
cat_image varchar // image representing the category
lesson_type_id integer [ref: > LessonTypes.id] // foreign key referencing LessonTypes.id
}
Table Helloworlds {
// system field
id int [pk, increment] // id field, increment
created datetime [default: `now()`] // record create time
updated datetime // record update time
}
Table Users {
// system field
id int [pk, increment]
created datetime [default: `now()`]
updated datetime
// value field
email varchar
emailVisibility boolean
verified boolean
name varchar
avatar blob
}
Table UserMetas {
// system field
id int [pk, increment]
created datetime [default: `now()`]
updated datetime
// value field
helloworld varchar
app_on_time_s integer
user_id integer [ref: > Users.id]
}
Table QuizCategories {
// system field
id int [pk, increment]
created datetime [default: `now()`]
updated datetime
// value field
cat_name varchar // category name
cat_image varchar // category image
}
// stores all questions of matching frenzy
Table QuizMatchings {
// system field
id int [pk, increment] // id field, increment
created datetime [default: `now()`] // record create time
updated datetime // record update time
// value field
word varchar // modal answer
word_c varchar // question
cat_id integer [ref: > QuizCategories.id] // foreign key to QuizCategories.id
}
// QuizListening stores all listening quiz data
Table QuizListenings {
// system field
id int [pk, increment] // id field, increment
created datetime [default: `now()`] // record create time
updated datetime // record update time
// value field
sound varchar // URL to the sound file
word varchar // The word in the quiz
cat_id integer [ref: > QuizCategories.id]
}
// stores all categories of connectives revision quiz
Table QuizConnectivesCategories {
// system field
id int [pk, increment] // id field, increment
created datetime [default: `now()`] // record create time
updated datetime // record update time
// value field
cat_name varchar // category name
cat_image varchar // category image
}
// stores all questions of connectives revision quiz
Table QuizConnectives {
// system field
id int [pk, increment] // id field, increment
created datetime [default: `now()`] // record create time
updated datetime // record update time
// value field
question_fh varchar // first half
question_sh varchar // second half
modal_ans varchar // modal ans
cat_id integer [ref: > QuizConnectivesCategories.id] // foreign key to QuizConnectivesCategories.id
}
// Lessons stores all lessons in the database
Table Vocabularies {
// system field
id int [pk, increment] // unique identifier for the lesson
created datetime [default: `now()`] // timestamp when the lesson was created
updated datetime // timestamp when the lesson was last updated
// value field
image varchar // URL to the image associated with the lesson
sound varchar // URL to the sound file associated with the lesson
word varchar // The word in English
word_c varchar // The word in Chinese
sample_e varchar // Sample sentence in English using the word
sample_c varchar // Sample sentence in Chinese using the word
cat_id integer [ref: > LessonCategories.id] // foreign key referring to LessonCategories.id
category varchar // The category to which the lesson belongs
lesson_type_id integer [ref: > LessonTypes.id] // foreign key referring to LessonTypes.id
}
// Listening Practice Quiz Categories
// store listening practice category, (LpCategories, LpCategory)
Table QuizLPCategories {
// system fields
id text [pk] // changed from int to text to match PocketBase
created datetime [default: `now()`]
updated datetime
// value fields
cat_name varchar [presentable: true] // added presentable flag
cat_image file // changed from blob to file type
pos number // changed from integer to number
init_answer json
}
// Listening Practice Quiz Questions
Table QuizLPQuestions {
id int [pk, increment]
created datetime [default: `now()`]
updated datetime
word varchar
sound blob
cat_id integer [ref: > QuizLPCategories.id]
}
// Matching Frenzy Quiz Categories
Table QuizMFCategories {
id int [pk, increment]
created datetime [default: `now()`]
updated datetime
cat_name varchar
cat_image blob
pos integer
init_answer json
}
// Matching Frenzy Quiz Questions
Table QuizMFQuestions {
id int [pk, increment]
created datetime [default: `now()`]
updated datetime
word varchar
word_c varchar
cat_id integer [ref: > QuizMFCategories.id]
}
// Connectives Revision Quiz Categories
Table QuizCRCategories {
id int [pk, increment]
created datetime [default: `now()`]
updated datetime
cat_name varchar
cat_image blob
pos integer
init_answer json
}
// Connectives Revision Quiz Questions
Table QuizCRQuestions {
id int [pk, increment]
created datetime [default: `now()`]
updated datetime
question_fh varchar
question_sh varchar
modal_ans varchar
cat_id integer [ref: > QuizCRCategories.id]
}
// Test table
Table t1 {
id int [pk, increment]
created datetime [default: `now()`]
updated datetime
name varchar
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
---
tags: cms
---
# cms dashboard
cms
## files
T.B.A.

View File

@@ -0,0 +1,11 @@
---
tags: mobile
---
# mobile client
mobile
## files
T.B.A.

View File

@@ -0,0 +1,86 @@
//
// # REQ0015:
// ## PURPOSE:
//
// This is a script to convert `schema.json` exported from pocketbase to dbml formatted file `schema.dbml`
//
//
const fs = require("fs");
const path = require("path");
// Load schema.json
const schemaPath = path.join(process.cwd(), "schema.json");
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
// Type mapping from PocketBase to DBML/SQL
const typeMapping = {
text: "text",
number: "integer",
bool: "boolean",
email: "text",
url: "varchar",
date: "datetime",
select: "varchar",
json: "text",
file: "file",
password: "varchar",
relation: "integer", // Assuming relations are stored as integer IDs
autodate: "datetime",
};
function convertToDBML(schema) {
let dbml = "";
dbml = "// Generated at: " + new Date().toISOString();
dbml = dbml + "\n\n\n";
let collectionIdToTableMapping = {};
for (const [idx, tableDef] of Object.entries(schema)) {
collectionIdToTableMapping[tableDef.id] = tableDef.name;
}
for (const [idx, tableDef] of Object.entries(schema)) {
console.log({ idx });
dbml += `// \n`;
dbml += `// collection id: ${tableDef.id} \n`;
dbml += `// collection name: ${tableDef.name} \n`;
dbml += `// collection type: ${tableDef.type} \n`;
dbml += `Table ${tableDef.name} {\n`;
for (const field of tableDef.fields) {
// Get field type
let fieldType = typeMapping[field.type] || "text";
// Handle special cases
if (field.type === "number" && field.options?.max === 1) {
fieldType = "boolean";
}
// Build field definition
let line = ` ${field.name} ${fieldType}`;
// Add constraints
const constraints = [];
if (field.name === "id" || field.primary) constraints.push("pk");
if (field.required) constraints.push("not null");
if (constraints.length > 0) line += ` [${constraints.join(", ")}]`;
if (field.collectionId) {
line += ` [ref: > ${
collectionIdToTableMapping[field.collectionId]
}.id] // ${field.id}`;
}
dbml += `${line}\n`;
console.log({ line });
}
dbml += "}\n\n";
}
return dbml;
}
// Convert and save
const dbmlOutput = convertToDBML(schema);
fs.writeFileSync("schema.dbml", dbmlOutput);
console.log("DBML file generated successfully!");

View File

@@ -0,0 +1,9 @@
---
tags: mobile, cms, db, schema
---
# pocketbase json schema to dbml converter
## description
to generate a `schema.dbml` form `schema.json` exported from pocketbase

View File

@@ -0,0 +1,18 @@
// Generated at: 2025-05-06T06:24:44.009Z
//
// collection id: pbc_3142635823
// collection name: helloworld
// collection type: auth
Table helloworld {
id text [pk, not null]
password varchar [not null]
tokenKey text [not null]
email text [not null]
emailVisibility boolean
verified boolean
created datetime
updated datetime
}

View File

@@ -0,0 +1,174 @@
[
{
"id": "pbc_3142635823",
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"name": "helloworld",
"type": "auth",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cost": 0,
"hidden": true,
"id": "password901924565",
"max": 0,
"min": 8,
"name": "password",
"pattern": "",
"presentable": false,
"required": true,
"system": true,
"type": "password"
},
{
"autogeneratePattern": "[a-zA-Z0-9]{50}",
"hidden": true,
"id": "text2504183744",
"max": 60,
"min": 30,
"name": "tokenKey",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": true,
"type": "text"
},
{
"exceptDomains": null,
"hidden": false,
"id": "email3885137012",
"name": "email",
"onlyDomains": null,
"presentable": false,
"required": true,
"system": true,
"type": "email"
},
{
"hidden": false,
"id": "bool1547992806",
"name": "emailVisibility",
"presentable": false,
"required": false,
"system": true,
"type": "bool"
},
{
"hidden": false,
"id": "bool256245529",
"name": "verified",
"presentable": false,
"required": false,
"system": true,
"type": "bool"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": true,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": true,
"type": "autodate"
}
],
"indexes": [
"CREATE UNIQUE INDEX `idx_tokenKey_pbc_3142635823` ON `_superusers` (`tokenKey`)",
"CREATE UNIQUE INDEX `idx_email_pbc_3142635823` ON `_superusers` (`email`) WHERE `email` != ''"
],
"system": true,
"authRule": "",
"manageRule": null,
"authAlert": {
"enabled": true,
"emailTemplate": {
"subject": "Login from a new location",
"body": "<p>Hello,</p>\n<p>We noticed a login to your {APP_NAME} account from a new location.</p>\n<p>If this was you, you may disregard this email.</p>\n<p><strong>If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.</strong></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>"
}
},
"oauth2": {
"mappedFields": {
"id": "",
"name": "",
"username": "",
"avatarURL": ""
},
"enabled": false
},
"passwordAuth": {
"enabled": true,
"identityFields": [
"email"
]
},
"mfa": {
"enabled": false,
"duration": 1800,
"rule": ""
},
"otp": {
"enabled": false,
"duration": 180,
"length": 8,
"emailTemplate": {
"subject": "OTP for {APP_NAME}",
"body": "<p>Hello,</p>\n<p>Your one-time password is: <strong>{OTP}</strong></p>\n<p><i>If you didn't ask for the one-time password, you can ignore this email.</i></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>"
}
},
"authToken": {
"duration": 86400
},
"passwordResetToken": {
"duration": 1800
},
"emailChangeToken": {
"duration": 1800
},
"verificationToken": {
"duration": 259200
},
"fileToken": {
"duration": 180
},
"verificationTemplate": {
"subject": "Verify your {APP_NAME} email",
"body": "<p>Hello,</p>\n<p>Thank you for joining us at {APP_NAME}.</p>\n<p>Click on the button below to verify your email address.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-verification/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Verify</a>\n</p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>"
},
"resetPasswordTemplate": {
"subject": "Reset your {APP_NAME} password",
"body": "<p>Hello,</p>\n<p>Click on the button below to reset your password.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Reset password</a>\n</p>\n<p><i>If you didn't ask to reset your password, you can ignore this email.</i></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>"
},
"confirmEmailChangeTemplate": {
"subject": "Confirm your {APP_NAME} new email address",
"body": "<p>Hello,</p>\n<p>Click on the button below to confirm your new email address.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-email-change/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Confirm new email</a>\n</p>\n<p><i>If you didn't ask to change your email address, you can ignore this email.</i></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>"
}
}
]

View File

@@ -0,0 +1,33 @@
---
tags: cms, login-flow
---
# login flow
## description
```mermaid
graph TD;
Start-->A;
A-->B;
B-->C;
B-->D;
D-->E;
E-->F;
C-->G;
G-->A
F-->End;
A[greeting, asking username and password]
B[check if username and password is valid]
C[pasword failed]
D[pasword ok]
E[login success]
F[redirect to '/dashboard']
G[prompt user wrong username and password]
Start((start));
End((end))
```

View File

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 178 KiB

View File

@@ -0,0 +1,9 @@
---
tags: mobile, lesson
---
# lesson page documentation
it should have a lesson page
![alt text](image.png)

View File

@@ -0,0 +1,7 @@
---
tags: mobile, cms, db
---
# family photo of frameworks
it should have a family photo of used framework

View File

@@ -1,50 +1,21 @@
# Requirements Index
## REQ0001: project overview
[View Requirement](./REQ0001/index.md)
## REQ0002: lesson page documentation
[View Requirement](./REQ0002/index.md)
## REQ0003: quiz page documentation
[View Requirement](./REQ0003/index.md)
## REQ0004: record page documentation
[View Requirement](./REQ0004/index.md)
## REQ0005: settings page documentation
[View Requirement](./REQ0005/index.md)
## REQ0006: db schema design
[View Requirement](./REQ0006/index.md)
## REQ0007: testing of mobile app home
[View Requirement](./REQ0007/index.md)
## REQ0008: schema design
[View Requirement](./REQ0008/index.md)
## REQ0009: loading screen
[View Requirement](./REQ0009/index.md)
## REQ0010: user friendly error messages
[View Requirement](./REQ0010/index.md)
## REQ0011: developer friendly error messages
[View Requirement](./REQ0011/index.md)
## REQ0012: cms navigation system
[View Requirement](./REQ0012/index.md)
- [REQ0001: project overview](./REQ0001/index.md)
- [REQ0002: time tracking](./REQ0002/index.md)
- [REQ0003: quiz page documentation](./REQ0003/index.md)
- [REQ0004: record page documentation](./REQ0004/index.md)
- [REQ0005: settings page documentation](./REQ0005/index.md)
- [REQ0006: task](./REQ0006/008_update_dbml_from_json.md)
- [REQ0006: GUIDELINES](./REQ0006/_GUIDELINE.md)
- [REQ0006: db schema design](./REQ0006/index.md)
- [REQ0007: testing of mobile app home](./REQ0007/index.md)
- [REQ0008: schema design](./REQ0008/index.md)
- [REQ0009: loading screen](./REQ0009/index.md)
- [REQ0010: user friendly error messages](./REQ0010/index.md)
- [REQ0011: developer friendly error messages](./REQ0011/index.md)
- [REQ0012: cms navigation system](./REQ0012/index.md)
- [REQ0013: cms dashboard](./REQ0013/index.md)
- [REQ0014: mobile client](./REQ0014/index.md)
- [REQ0015: pocketbase json schema to dbml converter](./REQ0015/index.md)
- [REQ0016: login flow](./REQ0016/index.md)
- [REQ0017: lesson page documentation](./REQ0017/index.md)

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -ex
npx nodemon --delay 0.1 --ext md,js --ignore ./index.md --exec "node ./update_req_index.js"

View File

@@ -36,8 +36,8 @@ async function updateIndex() {
const heading = extractH1(content);
if (heading) {
indexContent += `## ${dir}: ${heading}\n\n`;
indexContent += `[View Requirement](./${dir}/${file})\n\n`;
indexContent += `- [${dir}: ${heading}](./${dir}/${file})\n`;
// indexContent += `[View Requirement](./${dir}/${file})\n\n`;
}
}
}

14
002_source/.env Normal file
View File

@@ -0,0 +1,14 @@
# THIS IS env file for use with docker-compose.yml
# cms
# doc
# ionic_mobile
# api_ts
# pocketbase
PB_HOSTNAME=pocketbase
PB_USERNAME=admin@123.com
PB_PASSWORD=Aa12345678

4
002_source/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
**/*.del
**/*.log
**/_archive
**/_del

74
002_source/README.md Normal file
View File

@@ -0,0 +1,74 @@
# README
## to start production
```bash
# start docker before hand
$ ./dc_build.sh
$ ./dc_up.sh
```
## to start develop
```bash
#
$ ./dc_build.sh
$ ./dc_dev.sh
$ docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d pocketbase api_ts --remove-orphans
$ docker compose logs -f pocketbase api_ts
```
## test api endpoint
```bash
# browse to http://localhost:3000/helloworld
```
```bash
# start docker before hand
$ cd 002_source
$ ./dev.sh
# docker containers up
```
---
## deprecated
```bash
# mobile
$ cd 002_source/mobile
$ pnpm run dev
# cms
$ cd 002_source/cms
$ npm run dev
```
ideation
prototyping
testing
production deployment
evaluation
monitoring
## addresses
```
mobile:
http://localhost:5173
pocketbase:
http://localhost:8090/_
cms:
http://localhost:3000
documentation
http://localhost:3001
```
extend to vocabularies

View File

@@ -1,4 +1,5 @@
'use client';
// src/app/dashboard/students/edit/[customerId]/page.tsx
import * as React from 'react';
import RouterLink from 'next/link';

View File

@@ -3,7 +3,7 @@
import type { Customer } from '@/components/dashboard/customer/type.d';
import { dayjs } from '@/lib/dayjs';
export const SampleCustomers = [
export const SampleTeachers = [
{
id: 'USR-005',
name: 'Fran Perez',

View File

@@ -1,11 +1,11 @@
# GUIDELINES
this folder is part of nextjs typescript project and containing page definition for `Customer` / `Customers` record:
this folder is part of nextjs typescript project and containing page definition for `UserMeta` / `UserMetas` record:
- list (./page.tsx)
- view (./[customerId]/page.tsx)
- view (./[userMetaId]/page.tsx)
- create (./create/page.tsx)
- edit (./[customerId]/page.tsx)
- edit (./[userMetaId]/page.tsx)
- translation provided by react-i18next
the `@` sign refer to `<base_dir>/002_source/002_source/cms/src`
@@ -13,17 +13,17 @@ the `@` sign refer to `<base_dir>/002_source/002_source/cms/src`
## Assumption and Requirements
- let one file contains one component only.
- type information defined in `<base_dir>/002_source/cms/src/db/Customers/type.d.tsx`
- it mainly consume the db drivers `Customres` in `<base_dir>/002_source/cms/src/db/Customers`
- type information defined in `<base_dir>/002_source/cms/src/db/UserMetas/type.d.tsx`
- it mainly consume the db drivers `UserMetas` in `<base_dir>/002_source/cms/src/db/UserMetas`
simple template:
```typescript
// src/app/dashboard/customers/page.tsx
// src/app/dashboard/users/page.tsx
'use client';
// RULES:
// contains list page for customers (Customers)
// contains list page for user metas (UserMetas)
// contain definition to collection only
//
import statements here ...
@@ -44,6 +44,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
interface PageProps {
searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string };
searchParams: { userId?: string; key?: string; sortDir?: 'asc' | 'desc'; value?: string };
}
```

View File

@@ -1,5 +1,5 @@
'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';
@@ -9,9 +9,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import { config } from '@/config';
import { paths } from '@/paths';
import { UserCreateForm } from '@/components/dashboard/user/user-create-form';
export const metadata = { title: `Create | Users | Dashboard | ${config.site.name}` } satisfies Metadata;
import { TeacherCreateForm } from '@/components/dashboard/teacher/teacher-create-form';
export default function Page(): React.JSX.Element {
return (
@@ -29,19 +27,19 @@ export default function Page(): React.JSX.Element {
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.users.list}
href={paths.dashboard.teachers.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Users
Teachers
</Link>
</div>
<div>
<Typography variant="h4">Create user</Typography>
<Typography variant="h4">Create teacher</Typography>
</div>
</Stack>
<UserCreateForm />
<TeacherCreateForm />
</Stack>
</Box>
);

View File

@@ -0,0 +1,3 @@
this `tsx` file is clone from elsewhere, please understand, modify and update the content of `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/user_metas/edit/[id]/page.tsx.draft` to handle `UserMeta` record thanks, modify comments/variables/paths/functions name please
e.g. why `lessonCategories` still exist ?

View File

@@ -1,4 +1,5 @@
'use client';
// src/app/dashboard/user_metas/edit/[id]/page.tsx
import * as React from 'react';
import RouterLink from 'next/link';
@@ -11,10 +12,10 @@ import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form';
import { UserEditForm } from '@/components/dashboard/user/user-edit-form';
import { UserMetaEditForm } from '@/components/dashboard/user_meta/user-meta-edit-form';
export default function Page(): React.JSX.Element {
const { t } = useTranslation(['users']);
const { t } = useTranslation(['lp_categories']);
React.useEffect(() => {
// console.log('helloworld');
@@ -35,7 +36,7 @@ export default function Page(): React.JSX.Element {
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.users.list}
href={paths.dashboard.user_metas.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
@@ -47,7 +48,7 @@ export default function Page(): React.JSX.Element {
<Typography variant="h4">{t('edit.title')}</Typography>
</div>
</Stack>
<UserEditForm />
<UserMetaEditForm />
</Stack>
</Box>
);

View File

@@ -0,0 +1,3 @@
this `tsx` file is clone from elsewhere, please understand, modify and update the content of `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/user_metas/list/page.tsx.draft` to handle `UserMeta` record thanks, modify comments/variables/paths/functions name please
e.g. why `lessonCategories` still exist ?

View File

@@ -1,13 +1,12 @@
// src/app/dashboard/customers/page.tsx
// src/app/dashboard/user_metas/list/page.tsx
'use client';
// RULES:
// contains list page for customers (Customers)
// contain definition to collection only
// contains list page for user metas (UserMetas)
//
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { COL_USERS } from '@/constants';
import { COL_USER_METAS } from '@/constants';
import { LoadingButton } from '@mui/lab';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
@@ -17,11 +16,11 @@ import Typography from '@mui/material/Typography';
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
import type { ListResult, RecordModel } from 'pocketbase';
import { UsersFilters } from '@/components/dashboard/user/users-filters';
import { UsersPagination } from '@/components/dashboard/user/users-pagination';
import { UsersSelectionProvider } from '@/components/dashboard/user/users-selection-context';
import { UsersTable } from '@/components/dashboard/user/users-table';
import type { User } from '@/components/dashboard/user/type.d';
import { UserMetasFilters } from '@/components/dashboard/user_meta/user-metas-filters';
import { UserMetasPagination } from '@/components/dashboard/user_meta/user-metas-pagination';
import { UserMetasSelectionProvider } from '@/components/dashboard/user_meta/user-metas-selection-context';
import { UserMetasTable } from '@/components/dashboard/user_meta/user-metas-table';
import type { UserMeta } from '@/components/dashboard/user_meta/type.d';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
@@ -29,17 +28,16 @@ import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultUser } from '@/components/dashboard/user/_constants';
import { defaultUserMeta } from '@/components/dashboard/user_meta/_constants';
import FormLoading from '@/components/loading';
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { t } = useTranslation(['customers']);
const { t } = useTranslation(['teachers']);
const router = useRouter();
const { email, phone, sortDir, status } = searchParams;
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<User[]>([]);
//
const [userMetaData, setUserMetaData] = React.useState<UserMeta[]>([]);
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(true);
@@ -47,27 +45,31 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
//
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
//
const [f, setF] = React.useState<User[]>([]);
const [f, setF] = React.useState<UserMeta[]>([]);
const [currentPage, setCurrentPage] = React.useState<number>(0);
//
const [recordCount, setRecordCount] = React.useState<number>(0);
const [listOption, setListOption] = React.useState({});
const [listOption, setListOption] = React.useState({ filter: '' });
const [listSort, setListSort] = React.useState({});
function isListOptionChanged() {
return JSON.stringify(listOption) === JSON.stringify({ filter: '' });
}
//
const reloadRows = async (): Promise<void> => {
try {
const models: ListResult<RecordModel> = await pb.collection(COL_USERS).getList(currentPage + 1, rowsPerPage, {});
const models: ListResult<RecordModel> = await pb
.collection(COL_USER_METAS)
.getList(currentPage + 1, rowsPerPage, listOption);
const { items, totalItems } = models;
const tempLessonTypes: User[] = items.map((lt) => {
return { ...defaultUser, ...lt };
const tempUserMeta: UserMeta[] = items.map((lt) => {
return { ...defaultUserMeta, ...lt };
});
setLessonCategoriesData(tempLessonTypes);
setUserMetaData(tempUserMeta);
setRecordCount(totalItems);
setF(tempLessonTypes);
setF(tempUserMeta);
} catch (error) {
//
logger.error(error);
setShowError({
//
@@ -95,7 +97,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
}, [currentPage, rowsPerPage, listOption]);
React.useEffect(() => {
const tempFilter = [];
let tempFilter = [];
let tempSortDir = '';
if (status) {
@@ -109,11 +111,12 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
if (email) {
tempFilter.push(`email ~ "%${email}%"`);
}
if (phone) {
tempFilter.push(`phone ~ "%${phone}%"`);
}
let preFinalListOption = {};
let preFinalListOption = { filter: '' };
if (tempFilter.length > 0) {
preFinalListOption = { filter: tempFilter.join(' && ') };
}
@@ -157,7 +160,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
loading={isLoadingAddPage}
onClick={(): void => {
setIsLoadingAddPage(true);
router.push(paths.dashboard.users.create);
router.push(paths.dashboard.user_metas.create);
}}
startIcon={<PlusIcon />}
variant="contained"
@@ -166,22 +169,22 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
</LoadingButton>
</Box>
</Stack>
<UsersSelectionProvider users={f}>
<UserMetasSelectionProvider userMetas={f}>
<Card>
<UsersFilters
<UserMetasFilters
filters={{ email, phone, status }}
fullData={lessonCategoriesData}
fullData={userMetaData}
sortDir={sortDir}
/>
<Divider />
<Box sx={{ overflowX: 'auto' }}>
<UsersTable
<UserMetasTable
reloadRows={reloadRows}
rows={f}
/>
</Box>
<Divider />
<UsersPagination
<UserMetasPagination
count={recordCount}
page={currentPage}
rowsPerPage={rowsPerPage}
@@ -189,7 +192,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
setRowsPerPage={setRowsPerPage}
/>
</Card>
</UsersSelectionProvider>
</UserMetasSelectionProvider>
</Stack>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify(f, null, 2)}</pre>

View File

@@ -1,46 +1,7 @@
import * as React from 'react';
import type { Thread } from '@/components/dashboard/teacher/mail/types';
import { dayjs } from '@/lib/dayjs';
import { MailProvider } from '@/components/dashboard/mail/mail-context';
import { MailView } from '@/components/dashboard/mail/mail-view';
import type { Label, Thread } from '@/components/dashboard/mail/types';
function filterThreads(threads: Thread[], labelId: string): Thread[] {
return threads.filter((thread) => {
if (['inbox', 'sent', 'drafts', 'spam', 'trash'].includes(labelId)) {
return thread.folder === labelId;
}
if (labelId === 'important') {
return thread.isImportant;
}
if (labelId === 'starred') {
return thread.isStarred;
}
if (thread.labels.includes(labelId)) {
return true;
}
return false;
});
}
const labels = [
{ id: 'inbox', type: 'system', name: 'Inbox', unreadCount: 1, totalCount: 0 },
{ id: 'sent', type: 'system', name: 'Sent', unreadCount: 0, totalCount: 0 },
{ id: 'drafts', type: 'system', name: 'Drafts', unreadCount: 0, totalCount: 0 },
{ id: 'spam', type: 'system', name: 'Spam', unreadCount: 0, totalCount: 0 },
{ id: 'trash', type: 'system', name: 'Trash', unreadCount: 0, totalCount: 1 },
{ id: 'important', type: 'system', name: 'Important', unreadCount: 0, totalCount: 1 },
{ id: 'starred', type: 'system', name: 'Starred', unreadCount: 1, totalCount: 1 },
{ id: 'work', type: 'custom', name: 'Work', color: '#43A048', unreadCount: 0, totalCount: 1 },
{ id: 'business', type: 'custom', name: 'Business', color: '#1E88E5', unreadCount: 1, totalCount: 2 },
{ id: 'personal', type: 'custom', name: 'Personal', color: '#FB8A00', unreadCount: 0, totalCount: 1 },
] satisfies Label[];
const threads = [
export const SampleThreads = [
{
id: 'TRD-004',
from: { avatar: '/assets/avatar-9.png', email: 'marcus.finn@domain.com', name: 'Marcus Finn' },
@@ -123,20 +84,3 @@ Your flight is coming up soon. Please don't forget to check in for your schedule
createdAt: dayjs().subtract(2, 'day').toDate(),
},
] satisfies Thread[];
interface LayoutProps {
children: React.ReactNode;
params: { labelId: string };
}
export default function Layout({ children, params }: LayoutProps): React.JSX.Element {
const { labelId } = params;
const filteredThreads = filterThreads(threads, labelId);
return (
<MailProvider currentLabelId={labelId} labels={labels} threads={filteredThreads}>
<MailView>{children}</MailView>
</MailProvider>
);
}

View File

@@ -2,7 +2,7 @@ import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { ThreadView } from '@/components/dashboard/mail/thread-view';
import { ThreadView } from '@/components/dashboard/teacher/mail/thread-view';
export const metadata = { title: `Thread | Mail | Dashboard | ${config.site.name}` } satisfies Metadata;

View File

@@ -0,0 +1,62 @@
import * as React from 'react';
import { MailProvider } from '@/components/dashboard/teacher/mail/mail-context';
import { MailView } from '@/components/dashboard/teacher/mail/mail-view';
import type { Label, Thread } from '@/components/dashboard/teacher/mail/types';
import { SampleThreads } from './SampleThreads';
function filterThreads(threads: Thread[], labelId: string): Thread[] {
return threads.filter((thread) => {
if (['inbox', 'sent', 'drafts', 'spam', 'trash'].includes(labelId)) {
return thread.folder === labelId;
}
if (labelId === 'important') {
return thread.isImportant;
}
if (labelId === 'starred') {
return thread.isStarred;
}
if (thread.labels.includes(labelId)) {
return true;
}
return false;
});
}
const labels = [
{ id: 'inbox', type: 'system', name: 'Inbox', unreadCount: 1, totalCount: 0 },
{ id: 'sent', type: 'system', name: 'Sent', unreadCount: 0, totalCount: 0 },
{ id: 'drafts', type: 'system', name: 'Drafts', unreadCount: 0, totalCount: 0 },
{ id: 'spam', type: 'system', name: 'Spam', unreadCount: 0, totalCount: 0 },
{ id: 'trash', type: 'system', name: 'Trash', unreadCount: 0, totalCount: 1 },
{ id: 'important', type: 'system', name: 'Important', unreadCount: 0, totalCount: 1 },
{ id: 'starred', type: 'system', name: 'Starred', unreadCount: 1, totalCount: 1 },
{ id: 'work', type: 'custom', name: 'Work', color: '#43A048', unreadCount: 0, totalCount: 1 },
{ id: 'business', type: 'custom', name: 'Business', color: '#1E88E5', unreadCount: 1, totalCount: 2 },
{ id: 'personal', type: 'custom', name: 'Personal', color: '#FB8A00', unreadCount: 0, totalCount: 1 },
] satisfies Label[];
interface LayoutProps {
children: React.ReactNode;
params: { labelId: string };
}
export default function Layout({ children, params }: LayoutProps): React.JSX.Element {
const { labelId } = params;
const filteredThreads = filterThreads(SampleThreads, labelId);
return (
<MailProvider
currentLabelId={labelId}
labels={labels}
threads={filteredThreads}
>
<MailView>{children}</MailView>
</MailProvider>
);
}

View File

@@ -2,7 +2,7 @@ import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { ThreadsView } from '@/components/dashboard/mail/threads-view';
import { ThreadsView } from '@/components/dashboard/teacher/mail/threads-view';
export const metadata = { title: `Mail | Dashboard | ${config.site.name}` } satisfies Metadata;

View File

@@ -0,0 +1,11 @@
import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { ThreadsView } from '@/components/dashboard/teacher/mail/threads-view';
export const metadata = { title: `Mail | Dashboard | ${config.site.name}` } satisfies Metadata;
export default function Page(): React.JSX.Element {
return <ThreadsView />;
}

View File

@@ -14,13 +14,13 @@ 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';
import type { User } from '@/components/dashboard/user/type.d';
import type { UserMeta } from '@/components/dashboard/user_meta/type.d';
export default function BasicDetailCard({
user: model,
userMeta,
handleEditClick,
}: {
user: User;
userMeta: UserMeta;
handleEditClick: () => void;
}): React.JSX.Element {
const { t } = useTranslation();
@@ -55,15 +55,15 @@ export default function BasicDetailCard({
key: 'Customer ID',
value: (
<Chip
label={model.id}
label={userMeta.id}
size="small"
variant="soft"
/>
),
},
{ key: 'Email', value: model.email },
{ key: 'Quota', value: model.quota },
{ key: 'Status', value: model.status },
{ key: 'Email', value: userMeta.email },
{ key: 'Quota', value: userMeta.quota },
{ key: 'Status', value: userMeta.status },
] satisfies { key: string; value: React.ReactNode }[]
).map(
(item): React.JSX.Element => (

View File

@@ -9,17 +9,15 @@ 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 { Student } from '@/components/dashboard/student/type.d';
import { Teacher } from '@/components/dashboard/teacher/type.d';
// import type { CrCategory } from '@/components/dashboard/cr/categories/type';
function getImageUrlFrRecord(record: Student): string {
// TODO: fix this
// `http://127.0.0.1:8090/api/files/${'record.collectionId'}/${'record.id'}/${'record.cat_image'}`;
return 'getImageUrlFrRecord(helloworld)';
function getImageUrlFrRecord(teacher: Teacher): string {
return `http://127.0.0.1:8090/api/files/${teacher.collectionId}/${teacher.id}/${teacher.avatar}`;
}
export default function SampleTitleCard({ lpModel }: { lpModel: Student }): React.JSX.Element {
export default function SampleTitleCard({ teacherRecord }: { teacherRecord: Teacher }): React.JSX.Element {
const { t } = useTranslation();
return (
@@ -31,7 +29,7 @@ export default function SampleTitleCard({ lpModel }: { lpModel: Student }): Reac
>
<Avatar
variant="rounded"
src={getImageUrlFrRecord(lpModel)}
src={getImageUrlFrRecord(teacherRecord)}
sx={{ '--Avatar-size': '64px' }}
>
{t('empty')}
@@ -42,7 +40,7 @@ export default function SampleTitleCard({ lpModel }: { lpModel: Student }): Reac
spacing={2}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Typography variant="h4">{lpModel.email}</Typography>
<Typography variant="h4">{teacherRecord.name || teacherRecord.email}</Typography>
<Chip
icon={
<CheckCircleIcon
@@ -50,17 +48,11 @@ export default function SampleTitleCard({ lpModel }: { lpModel: Student }): Reac
weight="fill"
/>
}
label={lpModel.quota}
label={teacherRecord.status}
size="small"
variant="outlined"
/>
</Stack>
<Typography
color="text.secondary"
variant="body1"
>
{lpModel.status}
</Typography>
</div>
</Stack>
<div>

View File

@@ -0,0 +1,7 @@
working file: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/user_metas/view/[id]/page.tsx.draft`
this file is clone from elsewhere, please understand, modify and update the content of working file to handle `UserMeta` record thanks, modify comments/variables/paths/functions name please
restrict your modifications in this file only
e.g. why `lessonCategories` still exist ?

View File

@@ -24,35 +24,35 @@ import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { Notifications } from '@/components/dashboard/user/notifications';
import { Notifications } from '@/components/dashboard/user_meta/notifications';
import FormLoading from '@/components/loading';
import BasicDetailCard from './BasicDetailCard';
import TitleCard from './TitleCard';
import { defaultUser } from '@/components/dashboard/user/_constants';
import type { User } from '@/components/dashboard/user/type.d';
import { COL_USERS } from '@/constants';
import { defaultUserMeta } from '@/components/dashboard/user_meta/_constants';
import type { UserMeta } from '@/components/dashboard/user_meta/type.d';
import { COL_USER_METAS } from '@/constants';
export default function Page(): React.JSX.Element {
const { t } = useTranslation();
const router = useRouter();
//
const { id: userId } = useParams<{ id: string }>();
const { id } = useParams<{ id: string }>();
//
const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState({ show: false, detail: '' });
//
const [showUser, setShowUser] = React.useState<User>(defaultUser);
const [userMeta, setUserMeta] = React.useState<UserMeta>(defaultUserMeta);
function handleEditClick(): void {
router.push(paths.dashboard.users.edit(showUser.id));
router.push(paths.dashboard.user_metas.edit(userMeta.id));
}
React.useEffect(() => {
if (userId) {
pb.collection(COL_USERS)
.getOne(userId)
if (id) {
pb.collection(COL_USER_METAS)
.getOne(id)
.then((model: RecordModel) => {
setShowUser({ ...defaultUser, ...model });
setUserMeta({ ...defaultUserMeta, ...model });
})
.catch((err) => {
logger.error(err);
@@ -64,9 +64,8 @@ export default function Page(): React.JSX.Element {
setShowLoading(false);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId]);
}, [id]);
// return <>{JSON.stringify({ showError, showLessonCategory }, null, 2)}</>;
@@ -95,12 +94,12 @@ export default function Page(): React.JSX.Element {
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.users.list}
href={paths.dashboard.user_metas.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Users
User Metas
</Link>
</div>
<Stack
@@ -108,7 +107,7 @@ export default function Page(): React.JSX.Element {
spacing={3}
sx={{ alignItems: 'flex-start' }}
>
<TitleCard user={showUser} />
<TitleCard teacherRecord={userMeta} />
</Stack>
</Stack>
<Grid
@@ -121,7 +120,7 @@ export default function Page(): React.JSX.Element {
>
<Stack spacing={4}>
<BasicDetailCard
user={showUser}
userMeta={userMeta}
handleEditClick={handleEditClick}
/>
<SampleSecurityCard />

View File

@@ -1,11 +0,0 @@
# task
## instruction
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/_helloworld/page.tsx`
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_types/edit/[typeId]/page.tsx`
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_categories/edit/page.tsx`
please draft a tsx for showing error to user thanks,

View File

@@ -1,76 +0,0 @@
'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 { Student } from '@/components/dashboard/student/type.d';
// import type { CrCategory } from '@/components/dashboard/cr/categories/type';
function getImageUrlFrRecord(record: Student): string {
// TODO: fix this
// `http://127.0.0.1:8090/api/files/${'record.collectionId'}/${'record.id'}/${'record.cat_image'}`;
return 'getImageUrlFrRecord(helloworld)';
}
export default function SampleTitleCard({ user: lpModel }: { user: Student }): 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.email}</Typography>
<Chip
icon={
<CheckCircleIcon
color="var(--mui-palette-success-main)"
weight="fill"
/>
}
label={lpModel.quota}
size="small"
variant="outlined"
/>
</Stack>
<Typography
color="text.secondary"
variant="body1"
>
{lpModel.status}
</Typography>
</div>
</Stack>
<div>
<Button
endIcon={<CaretDownIcon />}
variant="contained"
>
{t('list.action')}
</Button>
</div>
</>
);
}

View File

@@ -1,80 +0,0 @@
'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';
import type { Student } from '@/components/dashboard/student/type.d';
export default function BasicDetailCard({
lpModel: model,
handleEditClick,
}: {
lpModel: Student;
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: 'Email', value: model.email },
{ key: 'Quota', value: model.quota },
{ key: 'Status', value: model.status },
] satisfies { key: string; value: React.ReactNode }[]
).map(
(item): React.JSX.Element => (
<PropertyItem
key={item.key}
name={item.key}
value={item.value}
/>
)
)}
</PropertyList>
</Card>
);
}

View File

@@ -1,142 +0,0 @@
'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 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 { config } from '@/config';
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 { Notifications } from '@/components/dashboard/student/notifications';
import FormLoading from '@/components/loading';
import BasicDetailCard from './BasicDetailCard';
import TitleCard from './TitleCard';
import { defaultStudent } from '@/components/dashboard/student/_constants';
import type { Student } from '@/components/dashboard/student/type.d';
import { COL_STUDENTS } from '@/constants';
export default function Page(): React.JSX.Element {
const { t } = useTranslation();
const router = useRouter();
//
const { customerId } = useParams<{ customerId: string }>();
//
const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState({ show: false, detail: '' });
//
const [showLessonCategory, setShowLessonCategory] = React.useState<Student>(defaultStudent);
function handleEditClick(): void {
router.push(paths.dashboard.students.edit(showLessonCategory.id));
}
React.useEffect(() => {
if (customerId) {
pb.collection(COL_STUDENTS)
.getOne(customerId)
.then((model: RecordModel) => {
setShowLessonCategory({ ...defaultStudent, ...model });
})
.catch((err) => {
logger.error(err);
toast(t('list.error'));
setShowError({ show: true, detail: JSON.stringify(err) });
})
.finally(() => {
setShowLoading(false);
});
}
}, [customerId]);
// return <>{JSON.stringify({ showError, showLessonCategory }, null, 2)}</>;
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.students.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Students
</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>
);
}

View File

@@ -29,6 +29,7 @@ export function AuthGuard({ children }: AuthGuardProps): React.JSX.Element | nul
return;
}
// NOTE: here state that if user = null, eject user to login page
if (!user) {
logger.debug('[AuthGuard]: User is not logged in, redirecting to sign in');

View File

@@ -0,0 +1,10 @@
# GUIDELINES
This folder contains login pages
the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src`
## Assumption and Requirements
- assume `pb` is located in `@/lib/pb`
- no need to handle error in this function, i'll handle it in the caller

View File

@@ -1,4 +1,6 @@
'use client';
// RULES:
// refer to ticket REQ0016 for login flow
import * as React from 'react';
import RouterLink from 'next/link';
@@ -25,6 +27,7 @@ import { authClient } from '@/lib/auth/custom/client';
import { useUser } from '@/hooks/use-user';
import { DynamicLogo } from '@/components/core/logo';
import { toast } from '@/components/core/toaster';
import { pb } from '@/lib/pb';
interface OAuthProvider {
id: 'google' | 'discord';
@@ -44,7 +47,7 @@ const schema = zod.object({
type Values = zod.infer<typeof schema>;
const defaultValues = { email: '', password: '' } satisfies Values;
const defaultValues = { email: 'admin@123.com', password: 'admin@123.com' } satisfies Values;
export function SignInForm(): React.JSX.Element {
const router = useRouter();
@@ -103,15 +106,31 @@ export function SignInForm(): React.JSX.Element {
return (
<Stack spacing={4}>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
<Box
component={RouterLink}
href={paths.home}
sx={{ display: 'inline-block', fontSize: 0 }}
>
<DynamicLogo
colorDark="light"
colorLight="dark"
height={32}
width={122}
/>
</Box>
</div>
<Stack spacing={1}>
<Typography variant="h5">Sign in</Typography>
<Typography color="text.secondary" variant="body2">
<Typography
color="text.secondary"
variant="body2"
>
Don&apos;t have an account?{' '}
<Link component={RouterLink} href={paths.auth.custom.signUp} variant="subtitle2">
<Link
component={RouterLink}
href={paths.auth.custom.signUp}
variant="subtitle2"
>
Sign up
</Link>
</Typography>
@@ -123,7 +142,15 @@ export function SignInForm(): React.JSX.Element {
<Button
color="secondary"
disabled={isPending}
endIcon={<Box alt="" component="img" height={24} src={provider.logo} width={24} />}
endIcon={
<Box
alt=""
component="img"
height={24}
src={provider.logo}
width={24}
/>
}
key={provider.id}
onClick={(): void => {
onAuth(provider.id).catch(() => {
@@ -147,7 +174,10 @@ export function SignInForm(): React.JSX.Element {
render={({ field }) => (
<FormControl error={Boolean(errors.email)}>
<InputLabel>Email address</InputLabel>
<OutlinedInput {...field} type="email" />
<OutlinedInput
{...field}
type="email"
/>
{errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null}
</FormControl>
)}
@@ -187,27 +217,65 @@ export function SignInForm(): React.JSX.Element {
)}
/>
{errors.root ? <Alert color="error">{errors.root.message}</Alert> : null}
<Button disabled={isPending} type="submit" variant="contained">
<Button
disabled={isPending}
type="submit"
variant="contained"
>
Sign in
</Button>
</Stack>
</form>
<div>
<Link component={RouterLink} href={paths.auth.custom.resetPassword} variant="subtitle2">
<Link
component={RouterLink}
href={paths.auth.custom.resetPassword}
variant="subtitle2"
>
Forgot password?
</Link>
</div>
</Stack>
</Stack>
<Alert color="warning">
Use{' '}
<Typography component="span" sx={{ fontWeight: 700 }} variant="inherit">
sofia@devias.io
</Typography>{' '}
with password{' '}
<Typography component="span" sx={{ fontWeight: 700 }} variant="inherit">
Secret1
</Typography>
<Stack>
<Box>
user:{' '}
<Typography
component="span"
sx={{ fontWeight: 700 }}
variant="inherit"
>
admin@123.com
</Typography>{' '}
password{' '}
<Typography
component="span"
sx={{ fontWeight: 700 }}
variant="inherit"
>
admin@123.com
</Typography>
</Box>
<Box>
user{' '}
<Typography
component="span"
sx={{ fontWeight: 700 }}
variant="inherit"
>
sofia@devias.io
</Typography>{' '}
password{' '}
<Typography
component="span"
sx={{ fontWeight: 700 }}
variant="inherit"
>
Secret1
</Typography>
</Box>
</Stack>
</Alert>
</Stack>
);

View File

@@ -115,15 +115,31 @@ export function SignInForm(): React.JSX.Element {
return (
<Stack spacing={4}>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
<Box
component={RouterLink}
href={paths.home}
sx={{ display: 'inline-block', fontSize: 0 }}
>
<DynamicLogo
colorDark="light"
colorLight="dark"
height={32}
width={122}
/>
</Box>
</div>
<Stack spacing={1}>
<Typography variant="h5">Sign in</Typography>
<Typography color="text.secondary" variant="body2">
<Typography
color="text.secondary"
variant="body2"
>
Don&apos;t have an account?{' '}
<Link component={RouterLink} href={paths.auth.supabase.signUp} variant="subtitle2">
<Link
component={RouterLink}
href={paths.auth.supabase.signUp}
variant="subtitle2"
>
Sign up
</Link>
</Typography>
@@ -135,7 +151,15 @@ export function SignInForm(): React.JSX.Element {
<Button
color="secondary"
disabled={isPending}
endIcon={<Box alt="" component="img" height={24} src={provider.logo} width={24} />}
endIcon={
<Box
alt=""
component="img"
height={24}
src={provider.logo}
width={24}
/>
}
key={provider.id}
onClick={(): void => {
onAuth(provider.id).catch(() => {
@@ -159,7 +183,10 @@ export function SignInForm(): React.JSX.Element {
render={({ field }) => (
<FormControl error={Boolean(errors.email)}>
<InputLabel>Email address</InputLabel>
<OutlinedInput {...field} type="email" />
<OutlinedInput
{...field}
type="email"
/>
{errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null}
</FormControl>
)}
@@ -199,13 +226,21 @@ export function SignInForm(): React.JSX.Element {
)}
/>
{errors.root ? <Alert color="error">{errors.root.message}</Alert> : null}
<Button disabled={isPending} type="submit" variant="contained">
<Button
disabled={isPending}
type="submit"
variant="contained"
>
Sign in
</Button>
</Stack>
</form>
<div>
<Link component={RouterLink} href={paths.auth.supabase.resetPassword} variant="subtitle2">
<Link
component={RouterLink}
href={paths.auth.supabase.resetPassword}
variant="subtitle2"
>
Forgot password?
</Link>
</div>

View File

@@ -116,9 +116,9 @@ export const layoutConfig = {
title: 'users',
icon: 'users',
items: [
{ key: 'users', title: 'List users', href: paths.dashboard.users.list },
{ key: 'users:create', title: 'Create user', href: paths.dashboard.users.create },
{ key: 'users:details', title: 'User details', href: paths.dashboard.users.view('1') },
{ key: 'users', title: 'List users', href: paths.dashboard.user_metas.list },
{ key: 'users:create', title: 'Create user', href: paths.dashboard.user_metas.create },
{ key: 'users:details', title: 'User details', href: paths.dashboard.user_metas.view('1') },
],
},
{

View File

@@ -112,9 +112,15 @@ export function ContactsPopover({ anchorEl, onClose, open = false }: ContactsPop
<Typography variant="h6">Contacts</Typography>
</Box>
<Box sx={{ maxHeight: '400px', overflowY: 'auto', px: 1, pb: 2 }}>
<List disablePadding sx={{ '& .MuiListItemButton-root': { borderRadius: 1 } }}>
<List
disablePadding
sx={{ '& .MuiListItemButton-root': { borderRadius: 1 } }}
>
{contacts.map((contact) => (
<ListItem disablePadding key={contact.id}>
<ListItem
disablePadding
key={contact.id}
>
<ListItemButton>
<ListItemAvatar>
<Avatar src={contact.avatar} />
@@ -122,14 +128,28 @@ export function ContactsPopover({ anchorEl, onClose, open = false }: ContactsPop
<ListItemText
disableTypography
primary={
<Link color="text.primary" noWrap underline="none" variant="subtitle2">
<Link
color="text.primary"
noWrap
underline="none"
variant="subtitle2"
>
{contact.name}
</Link>
}
/>
{contact.status !== 'offline' ? <Presence size="small" status={contact.status} /> : null}
{contact.status !== 'offline' ? (
<Presence
size="small"
status={contact.status}
/>
) : null}
{contact.status === 'offline' && Boolean(contact.lastActivity) ? (
<Typography color="text.secondary" sx={{ whiteSpace: 'nowrap' }} variant="caption">
<Typography
color="text.secondary"
sx={{ whiteSpace: 'nowrap' }}
variant="caption"
>
{dayjs(contact.lastActivity).fromNow()}
</Typography>
) : null}

View File

@@ -34,6 +34,7 @@ export function HorizontalLayout({ children }: HorizontalLayoutProps): React.JSX
color={settings.navColor}
items={layoutConfig.navItems}
/>
<Box
component="main"
sx={{

View File

@@ -36,15 +36,15 @@ import { Logo } from '@/components/core/logo';
import { SearchDialog } from '@/components/dashboard/layout/search-dialog';
import type { ColorScheme } from '@/styles/theme/types';
import { ContactsPopover } from '../contacts-popover';
import { languageFlags, LanguagePopover } from '../language-popover';
import type { Language } from '../language-popover';
import { MobileNav } from '../mobile-nav';
import { icons } from '../nav-icons';
import { NotificationsPopover } from '../notifications-popover';
import { UserPopover } from '../user-popover/user-popover';
import { WorkspacesSwitch } from '../workspaces-switch';
import { navColorStyles } from './styles';
import { ContactsPopover } from '../../contacts-popover';
import { languageFlags, LanguagePopover } from '../../language-popover';
import type { Language } from '../../language-popover';
import { MobileNav } from '../../mobile-nav';
import { icons } from '../../nav-icons';
import { NotificationsPopover } from '../../notifications-popover';
import { UserPopover } from '../../user-popover/user-popover';
import { WorkspacesSwitch } from '../../workspaces-switch';
import { navColorStyles } from '../styles';
const logoColors = {
dark: { blend_in: 'light', discrete: 'light', evident: 'light' },
@@ -92,7 +92,11 @@ export function MainNav({ color = 'evident', items = [] }: MainNavProps): React.
py: 1,
}}
>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flex: '1 1 auto' }}
>
<IconButton
onClick={(): void => {
setOpenNav(true);
@@ -101,8 +105,16 @@ export function MainNav({ color = 'evident', items = [] }: MainNavProps): React.
>
<ListIcon color="var(--NavItem-icon-color)" />
</IconButton>
<Box component={RouterLink} href={paths.home} sx={{ display: { xs: 'none', md: 'inline-block' } }}>
<Logo color={logoColor} height={32} width={122} />
<Box
component={RouterLink}
href={paths.home}
sx={{ display: { xs: 'none', md: 'inline-block' } }}
>
<Logo
color={logoColor}
height={32}
width={122}
/>
</Box>
<Box sx={{ display: { xs: 'none', md: 'block' } }}>
<WorkspacesSwitch />
@@ -154,11 +166,17 @@ function SearchButton(): React.JSX.Element {
return (
<React.Fragment>
<Tooltip title="Search">
<IconButton onClick={dialog.handleOpen} sx={{ display: { xs: 'none', md: 'inline-flex' } }}>
<IconButton
onClick={dialog.handleOpen}
sx={{ display: { xs: 'none', md: 'inline-flex' } }}
>
<MagnifyingGlassIcon color="var(--NavItem-icon-color)" />
</IconButton>
</Tooltip>
<SearchDialog onClose={dialog.handleClose} open={dialog.open} />
<SearchDialog
onClose={dialog.handleClose}
open={dialog.open}
/>
</React.Fragment>
);
}
@@ -174,12 +192,19 @@ function NotificationsButton(): React.JSX.Element {
sx={{ '& .MuiBadge-dot': { borderRadius: '50%', height: '10px', right: '6px', top: '6px', width: '10px' } }}
variant="dot"
>
<IconButton onClick={popover.handleOpen} ref={popover.anchorRef}>
<IconButton
onClick={popover.handleOpen}
ref={popover.anchorRef}
>
<BellIcon color="var(--NavItem-icon-color)" />
</IconButton>
</Badge>
</Tooltip>
<NotificationsPopover anchorEl={popover.anchorRef.current} onClose={popover.handleClose} open={popover.open} />
<NotificationsPopover
anchorEl={popover.anchorRef.current}
onClose={popover.handleClose}
open={popover.open}
/>
</React.Fragment>
);
}
@@ -190,11 +215,18 @@ function ContactsButton(): React.JSX.Element {
return (
<React.Fragment>
<Tooltip title="Contacts">
<IconButton onClick={popover.handleOpen} ref={popover.anchorRef}>
<IconButton
onClick={popover.handleOpen}
ref={popover.anchorRef}
>
<UsersIcon color="var(--NavItem-icon-color)" />
</IconButton>
</Tooltip>
<ContactsPopover anchorEl={popover.anchorRef.current} onClose={popover.handleClose} open={popover.open} />
<ContactsPopover
anchorEl={popover.anchorRef.current}
onClose={popover.handleClose}
open={popover.open}
/>
</React.Fragment>
);
}
@@ -214,11 +246,20 @@ function LanguageSwitch(): React.JSX.Element {
sx={{ display: { xs: 'none', md: 'inline-flex' } }}
>
<Box sx={{ height: '24px', width: '24px' }}>
<Box alt={language} component="img" src={flag} sx={{ height: 'auto', width: '100%' }} />
<Box
alt={language}
component="img"
src={flag}
sx={{ height: 'auto', width: '100%' }}
/>
</Box>
</IconButton>
</Tooltip>
<LanguagePopover anchorEl={popover.anchorRef.current} onClose={popover.handleClose} open={popover.open} />
<LanguagePopover
anchorEl={popover.anchorRef.current}
onClose={popover.handleClose}
open={popover.open}
/>
</React.Fragment>
);
}
@@ -259,7 +300,11 @@ function UserButton(): React.JSX.Element {
<Avatar src={user.avatar} />
</Badge>
</Box>
<UserPopover anchorEl={popover.anchorRef.current} onClose={popover.handleClose} open={popover.open} />
<UserPopover
anchorEl={popover.anchorRef.current}
onClose={popover.handleClose}
open={popover.open}
/>
</React.Fragment>
);
}
@@ -267,7 +312,11 @@ function UserButton(): React.JSX.Element {
function renderNavGroups({ items = [], pathname }: { items?: NavItemConfig[]; pathname: string }): React.JSX.Element {
const children = items.reduce((acc: React.ReactNode[], curr: NavItemConfig): React.ReactNode[] => {
acc.push(
<Box component="li" key={curr.key} sx={{ flex: '0 0 auto' }}>
<Box
component="li"
key={curr.key}
sx={{ flex: '0 0 auto' }}
>
{renderNavItems({ pathname, items: curr.items })}
</Box>
);
@@ -276,7 +325,12 @@ function renderNavGroups({ items = [], pathname }: { items?: NavItemConfig[]; pa
}, []);
return (
<Stack component="ul" direction="row" spacing={2} sx={{ listStyle: 'none', m: 0, p: '8px 12px' }}>
<Stack
component="ul"
direction="row"
spacing={2}
sx={{ listStyle: 'none', m: 0, p: '8px 12px' }}
>
{children}
</Stack>
);
@@ -286,13 +340,24 @@ function renderNavItems({ items = [], pathname }: { items?: NavItemConfig[]; pat
const children = items.reduce((acc: React.ReactNode[], curr: NavItemConfig): React.ReactNode[] => {
const { key, ...item } = curr;
acc.push(<NavItem key={key} pathname={pathname} {...item} />);
acc.push(
<NavItem
key={key}
pathname={pathname}
{...item}
/>
);
return acc;
}, []);
return (
<Stack component="ul" direction="row" spacing={2} sx={{ listStyle: 'none', m: 0, p: 0 }}>
<Stack
component="ul"
direction="row"
spacing={2}
sx={{ listStyle: 'none', m: 0, p: 0 }}
>
{children}
</Stack>
);
@@ -318,7 +383,10 @@ function NavItem({
const isBranch = Boolean(items);
const element = (
<Box component="li" sx={{ userSelect: 'none' }}>
<Box
component="li"
sx={{ userSelect: 'none' }}
>
<Box
{...(isBranch
? { role: 'button' }
@@ -373,10 +441,19 @@ function NavItem({
{title}
</Typography>
</Box>
{label ? <Chip color="primary" label={label} size="small" /> : null}
{label ? (
<Chip
color="primary"
label={label}
size="small"
/>
) : null}
{external ? (
<Box sx={{ alignItems: 'center', display: 'flex', flex: '0 0 auto' }}>
<ArrowSquareOutIcon color="var(--NavItem-icon-color)" fontSize="var(--icon-fontSize-sm)" />
<ArrowSquareOutIcon
color="var(--NavItem-icon-color)"
fontSize="var(--icon-fontSize-sm)"
/>
</Box>
) : null}
{isBranch ? (
@@ -415,13 +492,23 @@ function renderDropdownItems({
const children = items.reduce((acc: React.ReactNode[], curr: NavItemConfig): React.ReactNode[] => {
const { key, ...item } = curr;
acc.push(<DropdownItem key={key} pathname={pathname} {...item} />);
acc.push(
<DropdownItem
key={key}
pathname={pathname}
{...item}
/>
);
return acc;
}, []);
return (
<Stack component="ul" spacing={1} sx={{ listStyle: 'none', m: 0, p: 0 }}>
<Stack
component="ul"
spacing={1}
sx={{ listStyle: 'none', m: 0, p: 0 }}
>
{children}
</Stack>
);
@@ -444,7 +531,10 @@ function DropdownItem({
const isBranch = Boolean(items);
const element = (
<Box component="li" sx={{ userSelect: 'none' }}>
<Box
component="li"
sx={{ userSelect: 'none' }}
>
<Box
{...(isBranch
? { role: 'button' }

View File

@@ -37,7 +37,11 @@ export function CustomSignOut(): React.JSX.Element {
}, [checkSession, router]);
return (
<MenuItem component="div" onClick={handleSignOut} sx={{ justifyContent: 'center' }}>
<MenuItem
component="div"
onClick={handleSignOut}
sx={{ justifyContent: 'center' }}
>
Sign out
</MenuItem>
);

View File

@@ -23,8 +23,10 @@ import { CognitoSignOut } from './cognito-sign-out';
import { CustomSignOut } from './custom-sign-out';
import { FirebaseSignOut } from './firebase-sign-out';
import { SupabaseSignOut } from './supabase-sign-out';
import { authClient } from '@/lib/auth/custom/client';
import { logger } from '@/lib/default-logger';
const user = {
const defaultUser = {
id: 'USR-000',
name: 'Sofia Rivers',
avatar: '/assets/avatar.png',
@@ -38,6 +40,23 @@ export interface UserPopoverProps {
}
export function UserPopover({ anchorEl, onClose, open }: UserPopoverProps): React.JSX.Element {
const [userMeta, setUserMeta] = React.useState<User>(defaultUser);
async function loadUserMeta(): Promise<void> {
try {
const tempUserMeta = await authClient.getUser();
if (tempUserMeta.error) throw new Error(tempUserMeta.error);
setUserMeta(tempUserMeta.data as unknown as User);
} catch (error) {
logger.error(error);
}
}
React.useEffect(() => {
void loadUserMeta();
}, []);
if (!userMeta) return <>loading</>;
return (
<Popover
anchorEl={anchorEl}
@@ -48,26 +67,41 @@ export function UserPopover({ anchorEl, onClose, open }: UserPopoverProps): Reac
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
>
<Box sx={{ p: 2 }}>
<Typography>{user.name}</Typography>
<Typography color="text.secondary" variant="body2">
{user.email}
<Typography>{userMeta.name}</Typography>
<Typography
color="text.secondary"
variant="body2"
>
{userMeta.email}
</Typography>
</Box>
<Divider />
<List sx={{ p: 1 }}>
<MenuItem component={RouterLink} href={paths.dashboard.settings.account} onClick={onClose}>
<MenuItem
component={RouterLink}
href={paths.dashboard.settings.account}
onClick={onClose}
>
<ListItemIcon>
<UserIcon />
</ListItemIcon>
Account
</MenuItem>
<MenuItem component={RouterLink} href={paths.dashboard.settings.security} onClick={onClose}>
<MenuItem
component={RouterLink}
href={paths.dashboard.settings.security}
onClick={onClose}
>
<ListItemIcon>
<LockKeyIcon />
</ListItemIcon>
Security
</MenuItem>
<MenuItem component={RouterLink} href={paths.dashboard.settings.billing} onClick={onClose}>
<MenuItem
component={RouterLink}
href={paths.dashboard.settings.billing}
onClick={onClose}
>
<ListItemIcon>
<CreditCardIcon />
</ListItemIcon>

View File

@@ -1,216 +0,0 @@
'use client';
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Badge from '@mui/material/Badge';
import Box from '@mui/material/Box';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import Tooltip from '@mui/material/Tooltip';
import { Bell as BellIcon } from '@phosphor-icons/react/dist/ssr/Bell';
import { List as ListIcon } from '@phosphor-icons/react/dist/ssr/List';
import { MagnifyingGlass as MagnifyingGlassIcon } from '@phosphor-icons/react/dist/ssr/MagnifyingGlass';
import { Users as UsersIcon } from '@phosphor-icons/react/dist/ssr/Users';
import { useTranslation } from 'next-i18next';
import type { NavItemConfig } from '@/types/nav';
import type { User } from '@/types/user';
import { useDialog } from '@/hooks/use-dialog';
import { usePopover } from '@/hooks/use-popover';
import { ContactsPopover } from '../contacts-popover';
import { languageFlags, LanguagePopover } from '../language-popover';
import type { Language } from '../language-popover';
import { MobileNav } from '../mobile-nav';
import { NotificationsPopover } from '../notifications-popover';
import { SearchDialog } from '../search-dialog';
import { UserPopover } from '../user-popover/user-popover';
export interface MainNavProps {
items: NavItemConfig[];
}
export function MainNav({ items }: MainNavProps): React.JSX.Element {
const [openNav, setOpenNav] = React.useState<boolean>(false);
return (
<React.Fragment>
<Box
component="header"
sx={{
'--MainNav-background': 'var(--mui-palette-background-default)',
'--MainNav-divider': 'var(--mui-palette-divider)',
bgcolor: 'var(--MainNav-background)',
left: 0,
position: 'sticky',
pt: { lg: 'var(--Layout-gap)' },
top: 0,
width: '100%',
zIndex: 'var(--MainNav-zIndex)',
}}
>
<Box
sx={{
borderBottom: '1px solid var(--MainNav-divider)',
display: 'flex',
flex: '1 1 auto',
minHeight: 'var(--MainNav-height)',
px: { xs: 2, lg: 3 },
py: 1,
}}
>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
<IconButton
onClick={(): void => {
setOpenNav(true);
}}
sx={{ display: { lg: 'none' } }}
>
<ListIcon />
</IconButton>
<SearchButton />
</Stack>
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flex: '1 1 auto', justifyContent: 'flex-end' }}
>
<NotificationsButton />
<ContactsButton />
<Divider
flexItem
orientation="vertical"
sx={{ borderColor: 'var(--MainNav-divider)', display: { xs: 'none', lg: 'block' } }}
/>
<LanguageSwitch />
<UserButton />
</Stack>
</Box>
</Box>
<MobileNav
items={items}
onClose={() => {
setOpenNav(false);
}}
open={openNav}
/>
</React.Fragment>
);
}
function SearchButton(): React.JSX.Element {
const dialog = useDialog();
return (
<React.Fragment>
<Tooltip title="Search">
<IconButton onClick={dialog.handleOpen} sx={{ display: { xs: 'none', lg: 'inline-flex' } }}>
<MagnifyingGlassIcon />
</IconButton>
</Tooltip>
<SearchDialog onClose={dialog.handleClose} open={dialog.open} />
</React.Fragment>
);
}
function ContactsButton(): React.JSX.Element {
const popover = usePopover<HTMLButtonElement>();
return (
<React.Fragment>
<Tooltip title="Contacts">
<IconButton onClick={popover.handleOpen} ref={popover.anchorRef}>
<UsersIcon />
</IconButton>
</Tooltip>
<ContactsPopover anchorEl={popover.anchorRef.current} onClose={popover.handleClose} open={popover.open} />
</React.Fragment>
);
}
function NotificationsButton(): React.JSX.Element {
const popover = usePopover<HTMLButtonElement>();
return (
<React.Fragment>
<Tooltip title="Notifications">
<Badge
color="error"
sx={{ '& .MuiBadge-dot': { borderRadius: '50%', height: '10px', right: '6px', top: '6px', width: '10px' } }}
variant="dot"
>
<IconButton onClick={popover.handleOpen} ref={popover.anchorRef}>
<BellIcon />
</IconButton>
</Badge>
</Tooltip>
<NotificationsPopover anchorEl={popover.anchorRef.current} onClose={popover.handleClose} open={popover.open} />
</React.Fragment>
);
}
function LanguageSwitch(): React.JSX.Element {
const { i18n } = useTranslation();
const popover = usePopover<HTMLButtonElement>();
const language = (i18n.language || 'en') as Language;
const flag = languageFlags[language];
return (
<React.Fragment>
<Tooltip title="Language">
<IconButton
onClick={popover.handleOpen}
ref={popover.anchorRef}
sx={{ display: { xs: 'none', lg: 'inline-flex' } }}
>
<Box sx={{ height: '24px', width: '24px' }}>
<Box alt={language} component="img" src={flag} sx={{ height: 'auto', width: '100%' }} />
</Box>
</IconButton>
</Tooltip>
<LanguagePopover anchorEl={popover.anchorRef.current} onClose={popover.handleClose} open={popover.open} />
</React.Fragment>
);
}
const user = {
id: 'USR-000',
name: 'Sofia Rivers',
avatar: '/assets/avatar.png',
email: 'sofia@devias.io',
} satisfies User;
function UserButton(): React.JSX.Element {
const popover = usePopover<HTMLButtonElement>();
return (
<React.Fragment>
<Box
component="button"
onClick={popover.handleOpen}
ref={popover.anchorRef}
sx={{ border: 'none', background: 'transparent', cursor: 'pointer', p: 0 }}
>
<Badge
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
color="success"
sx={{
'& .MuiBadge-dot': {
border: '2px solid var(--MainNav-background)',
borderRadius: '50%',
bottom: '6px',
height: '12px',
right: '6px',
width: '12px',
},
}}
variant="dot"
>
<Avatar src={user.avatar} />
</Badge>
</Box>
<UserPopover anchorEl={popover.anchorRef.current} onClose={popover.handleClose} open={popover.open} />
</React.Fragment>
);
}

View File

@@ -0,0 +1,3 @@
# GUIDELINE
- please keep one default `export` per file

View File

@@ -0,0 +1,32 @@
'use client';
import * as React from 'react';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import { Users as UsersIcon } from '@phosphor-icons/react/dist/ssr/Users';
import { usePopover } from '@/hooks/use-popover';
import { ContactsPopover } from '../../contacts-popover';
export function ContactsButton(): React.JSX.Element {
const popover = usePopover<HTMLButtonElement>();
return (
<React.Fragment>
<Tooltip title="Contacts">
<IconButton
onClick={popover.handleOpen}
ref={popover.anchorRef}
>
<UsersIcon />
</IconButton>
</Tooltip>
<ContactsPopover
anchorEl={popover.anchorRef.current}
onClose={popover.handleClose}
open={popover.open}
/>
</React.Fragment>
);
}

View File

@@ -0,0 +1,94 @@
'use client';
import * as React from 'react';
import Box from '@mui/material/Box';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import { List as ListIcon } from '@phosphor-icons/react/dist/ssr/List';
import type { NavItemConfig } from '@/types/nav';
import { MobileNav } from '../../mobile-nav';
// import { NotificationsButton } from './notifications-button';
import { LanguageSwitch } from './language-switch';
import { ContactsButton } from './contacts-button';
import { SearchButton } from './search-button';
import { NotificationsButton } from './notifications-button';
import { UserButton } from './user-button';
export interface MainNavProps {
items: NavItemConfig[];
}
export function MainNav({ items }: MainNavProps): React.JSX.Element {
const [openNav, setOpenNav] = React.useState<boolean>(false);
return (
<React.Fragment>
<Box
component="header"
sx={{
'--MainNav-background': 'var(--mui-palette-background-default)',
'--MainNav-divider': 'var(--mui-palette-divider)',
bgcolor: 'var(--MainNav-background)',
left: 0,
position: 'sticky',
pt: { lg: 'var(--Layout-gap)' },
top: 0,
width: '100%',
zIndex: 'var(--MainNav-zIndex)',
}}
>
<Box
sx={{
borderBottom: '1px solid var(--MainNav-divider)',
display: 'flex',
flex: '1 1 auto',
minHeight: 'var(--MainNav-height)',
px: { xs: 2, lg: 3 },
py: 1,
}}
>
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flex: '1 1 auto' }}
>
<IconButton
onClick={(): void => {
setOpenNav(true);
}}
sx={{ display: { lg: 'none' } }}
>
<ListIcon />
</IconButton>
<SearchButton />
</Stack>
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flex: '1 1 auto', justifyContent: 'flex-end' }}
>
<NotificationsButton />
<ContactsButton />
<Divider
flexItem
orientation="vertical"
sx={{ borderColor: 'var(--MainNav-divider)', display: { xs: 'none', lg: 'block' } }}
/>
<LanguageSwitch />
<UserButton />
</Stack>
</Box>
</Box>
<MobileNav
items={items}
onClose={() => {
setOpenNav(false);
}}
open={openNav}
/>
</React.Fragment>
);
}

View File

@@ -0,0 +1,45 @@
'use client';
import * as React from 'react';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import { useTranslation } from 'next-i18next';
import { usePopover } from '@/hooks/use-popover';
import { languageFlags, LanguagePopover } from '../../language-popover';
import type { Language } from '../../language-popover';
export function LanguageSwitch(): React.JSX.Element {
const { i18n } = useTranslation();
const popover = usePopover<HTMLButtonElement>();
const language = (i18n.language || 'en') as Language;
const flag = languageFlags[language];
return (
<React.Fragment>
<Tooltip title="Language">
<IconButton
onClick={popover.handleOpen}
ref={popover.anchorRef}
sx={{ display: { xs: 'none', lg: 'inline-flex' } }}
>
<Box sx={{ height: '24px', width: '24px' }}>
<Box
alt={language}
component="img"
src={flag}
sx={{ height: 'auto', width: '100%' }}
/>
</Box>
</IconButton>
</Tooltip>
<LanguagePopover
anchorEl={popover.anchorRef.current}
onClose={popover.handleClose}
open={popover.open}
/>
</React.Fragment>
);
}

View File

@@ -0,0 +1,40 @@
'use client';
import * as React from 'react';
import Badge from '@mui/material/Badge';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import { Bell as BellIcon } from '@phosphor-icons/react/dist/ssr/Bell';
import { usePopover } from '@/hooks/use-popover';
import { NotificationsPopover } from '../../notifications-popover';
// import { NotificationsButton } from './notifications-button';
export function NotificationsButton(): React.JSX.Element {
const popover = usePopover<HTMLButtonElement>();
return (
<React.Fragment>
<Tooltip title="Notifications">
<Badge
color="error"
sx={{ '& .MuiBadge-dot': { borderRadius: '50%', height: '10px', right: '6px', top: '6px', width: '10px' } }}
variant="dot"
>
<IconButton
onClick={popover.handleOpen}
ref={popover.anchorRef}
>
<BellIcon />
</IconButton>
</Badge>
</Tooltip>
<NotificationsPopover
anchorEl={popover.anchorRef.current}
onClose={popover.handleClose}
open={popover.open}
/>
</React.Fragment>
);
}

View File

@@ -0,0 +1,31 @@
'use client';
import * as React from 'react';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import { MagnifyingGlass as MagnifyingGlassIcon } from '@phosphor-icons/react/dist/ssr/MagnifyingGlass';
import { useDialog } from '@/hooks/use-dialog';
import { SearchDialog } from '../../search-dialog';
export function SearchButton(): React.JSX.Element {
const dialog = useDialog();
return (
<React.Fragment>
<Tooltip title="Search">
<IconButton
onClick={dialog.handleOpen}
sx={{ display: { xs: 'none', lg: 'inline-flex' } }}
>
<MagnifyingGlassIcon />
</IconButton>
</Tooltip>
<SearchDialog
onClose={dialog.handleClose}
open={dialog.open}
/>
</React.Fragment>
);
}

View File

@@ -0,0 +1,63 @@
'use client';
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Badge from '@mui/material/Badge';
import Box from '@mui/material/Box';
import type { User } from '@/types/user';
import { usePopover } from '@/hooks/use-popover';
import { UserPopover } from '../../user-popover/user-popover';
import { useUser } from '@/hooks/use-user';
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
// import { NotificationsButton } from './notifications-button';
// TODO:remove me
const user1 = {
id: 'USR-000',
name: 'Sofia Rivers',
avatar: '/assets/avatar.png',
email: 'sofia@devias.io',
} satisfies User;
export function UserButton(): React.JSX.Element {
const popover = usePopover<HTMLButtonElement>();
const { user, error, isLoading } = useUser();
if (!user) return <>loading</>;
return (
<React.Fragment>
<Box
component="button"
onClick={popover.handleOpen}
ref={popover.anchorRef}
sx={{ border: 'none', background: 'transparent', cursor: 'pointer', p: 0 }}
>
<Badge
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
color="success"
sx={{
'& .MuiBadge-dot': {
border: '2px solid var(--MainNav-background)',
borderRadius: '50%',
bottom: '6px',
height: '12px',
right: '6px',
width: '12px',
},
}}
variant="dot"
>
<Avatar src={getImageUrlFromFile(user.collectionId, user.id, user.avatar)} />
</Badge>
</Box>
<UserPopover
anchorEl={popover.anchorRef.current}
onClose={popover.handleClose}
open={popover.open}
/>
</React.Fragment>
);
}

View File

@@ -0,0 +1,9 @@
'use client';
import type { User } from '@/types/user';
export const user = {
id: 'USR-000',
name: 'Sofia Rivers',
avatar: '/assets/avatar.png',
email: 'sofia@devias.io',
} satisfies User;

Some files were not shown because too many files have changed in this diff Show More