Compare commits

...

4 Commits

Author SHA1 Message Date
louiscklaw
7105bc85e3 update, 2025-05-08 14:38:34 +08:00
louiscklaw
61c10821b6 update, 2025-05-08 14:38:30 +08:00
louiscklaw
ea3f99654d update users, 2025-05-08 14:38:18 +08:00
louiscklaw
30f494fc35 update teachers, 2025-05-08 14:37:48 +08:00
118 changed files with 3172 additions and 5899 deletions

View File

@@ -1,3 +1,8 @@
**/*del
**/*bak
**/*log
**/*tmp
.env
.env.production

View File

@@ -1,9 +1,12 @@
# TODO
need to fix local storage error
page right (next page) button is not working
in the middle of clone lesson type to lesson category
- [ ] listing page
- [ ] delete button on each row
- [ ] create page
- [ ] edit
- [ ] delete
- [ ] listing page
- [ ] delete button on each row
- [ ] create page
- [ ] edit
- [ ] delete

View File

@@ -1,15 +0,0 @@
# guideline
- please divide the problem into small parts
- if you found youself cannot understand the problem, please stop and ask how to do
- if you found youself cannot solve the problem, plesae stop and ask how to do
- review the whole solution before you reply to user
- if code syntax is already there, do follow (e.g. naming convention, syntax) the existing code
- example for page can be found in `./src/app/_helloworld/page.tsx`
- example for component can be found in `./src/components/_helloworld/index.tsx`
- no need to explain the reason until you are told to do so
- no need to show me the code change, at the end just simple summary in point form is ok
Thanks

View File

@@ -1,44 +0,0 @@
# AI GUIDELINE
## getting started
Imagine there is a:
1. developer (provide the modification)
2. QA engineer (provide the feedback, and testing)
3. software engineer
4. technical writer
they will:
- conclude and integrate the ideas from developer and QA engineer
- make decision to modify the code accordingly.
## project background and initial setup
- No need to reply me what you are going on and your digest in this phase.
Just reply me "OK" when done
- base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
- `schema.dbml`
- read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
this is file in `dbml` format stating the main database structure
- `schema.json`
- read `<base_dir>/002_source/cms/src/db/schema.json`
this is the file of current pocketbase schema
- read `<base_dir>/002_source/cms/src/constants.ts`
this is the content of `@/constants`
- look into the md files in folder `<base_dir>/002_source/cms/_AI_WORKSPACE/001_guideline`
- directory may contain `repomix-output.xml` file, that is a simple summary of all files inside the directory
- 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

View File

@@ -1,52 +0,0 @@
---
description: Next.js with TypeScript and Tailwind UI best practices
globs: **/*.tsx, **/*.ts, src/**/*.ts, src/**/*.tsx
---
# Next.js Best Practices
## Project Structure
- Use the App Router directory structure
- Place components in `app` directory for route-specific components
- Place shared components in `components` directory
- Place utilities and helpers in `lib` directory
- Use lowercase with dashes for directories (e.g., `components/auth-wizard`)
## Components
- Use Server Components by default
- Mark client components explicitly with 'use client'
- Wrap client components in Suspense with fallback
- Use dynamic loading for non-critical components
- Implement proper error boundaries
- Place static content and interfaces at file end
## Performance
- Optimize images: Use WebP format, size data, lazy loading
- Minimize use of 'useEffect' and 'setState'
- Favor Server Components (RSC) where possible
- Use dynamic loading for non-critical components
- Implement proper caching strategies
## Data Fetching
- Use Server Components for data fetching when possible
- Implement proper error handling for data fetching
- Use appropriate caching strategies
- Handle loading and error states appropriately
## Routing
- Use the App Router conventions
- Implement proper loading and error states for routes
- Use dynamic routes appropriately
- Handle parallel routes when needed
## Forms and Validation
- Use Zod for form validation
- Implement proper server-side validation
- Handle form errors appropriately
- Show loading states during form submission
## State Management
- Minimize client-side state
- Use React Context sparingly
- Prefer server state when possible
- Implement proper loading states

View File

@@ -1,20 +0,0 @@
# task
update app page to cover `vocabulary`
## steps
1. read `tsx` from folder: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/vocabularies.draft`
1. fix the paths, variable, function names, class etc from `lesson-categories` to `vocabularies`.
thanks
## FAQ
1.`constants.ts` 中是否已定义 `COL_VOCABULARIES` 常量?没有,需要先定义它。
2. Vocabulary 相关的类型定义在哪里?是在现有文件中还是需要新建? 需要新建
3. 是否需要保留原始 `lessonCategories` 驱动文件? 不需要
- `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/vocabularies.draft`
- `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/vocabulary.draft`

View File

@@ -1,57 +0,0 @@
---
description: TypeScript coding standards and best practices for modern web development
globs: **/*.ts, **/*.tsx, **/*.d.ts
---
# TypeScript Best Practices
## Type System
- Prefer interfaces over types for object definitions
- Use type for unions, intersections, and mapped types
- Avoid using `any`, prefer `unknown` for unknown types
- Use strict TypeScript configuration
- Leverage TypeScript's built-in utility types
- Use generics for reusable type patterns
## Naming Conventions
- Use PascalCase for type names and interfaces
- Use camelCase for variables and functions
- Use UPPER_CASE for constants
- Use descriptive names with auxiliary verbs (e.g., isLoading, hasError)
- Prefix interfaces for React props with 'Props' (e.g., ButtonProps)
## Code Organization
- Keep type definitions close to where they're used
- Export types and interfaces from dedicated type files when shared
- Use barrel exports (index.ts) for organizing exports
- Place shared types in a `types` directory
- Co-locate component props with their components
## Functions
- Use explicit return types for public functions
- Use arrow functions for callbacks and methods
- Implement proper error handling with custom error types
- Use function overloads for complex type scenarios
- Prefer async/await over Promises
## Best Practices
- Enable strict mode in tsconfig.json
- Use readonly for immutable properties
- Leverage discriminated unions for type safety
- Use type guards for runtime type checking
- Implement proper null checking
- Avoid type assertions unless necessary
## Error Handling
- Create custom error types for domain-specific errors
- Use Result types for operations that can fail
- Implement proper error boundaries
- Use try-catch blocks with typed catch clauses
- Handle Promise rejections properly
## Patterns
- Use the Builder pattern for complex object creation
- Implement the Repository pattern for data access
- Use the Factory pattern for object creation
- Leverage dependency injection
- Use the Module pattern for encapsulation

View File

@@ -1,13 +0,0 @@
---
tags: update-constants-file
---
# task
update constants file
## steps
- have a look to `_constants.ts` files in slibing directory
- get the convention
- update `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/vocabulary/_constants.ts` according to dbml thanks

View File

@@ -1,13 +0,0 @@
---
tags: review-function-names
---
# task
review function names
## steps
- have a look to `tsx` files in slibing directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Vocabularies`
- get the convention
- update the function name of `tsx` file in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Vocabularies`

View File

@@ -1,22 +0,0 @@
Hi, i need your help.
i am working on a nextjs react typescript project
please read the code in directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPQuestions` (denoted `QuizLPQuestions`)
i want you to:
- list files from directory `QuizLPQuestions` and use them as template
- understand the relations (e.g. `lp` -> `lp_categories`, `lp` -> `COL_QUIZ_LP_CATEGORIES`)
- review if any remaining `lp` or `mf` exist in `cr` directory and please help to do replace it to `cr`. (e.g. `COL_QUIZ_LP_CATEGORIES` -> `COL_QUIZ_CR_CATEGORIES` )
- please create if you find any missing files/codes/constants
- using template, create the similar code for `cr` named `QuizCRCategories`
thank you
---
- I proofed the code is working already, what you need to do is refactoring of the `variables`, `functions`, `classes`, `constants` (`lp` -> `cr`)
- compare the difference between `lp` and `mf`,
- remember the differences and
- draft the new type `cr` (e.g. modify the `import` locations, variables, functions, classes, constants name etc.)

View File

@@ -1,22 +0,0 @@
Hi, i need your help.
i am working on a nextjs react typescript project
please read the code in directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPCategories` (denoted `QuizLPCategories`)
i want you to:
- list files from directory `QuizLPCategories` and use them as template
- understand the relations (e.g. `lp` -> `lp_categories`, `lp` -> `COL_QUIZ_LP_CATEGORIES`)
- review if any remaining `lp` or `mf` exist in `cr` directory and please help to do replace it to `cr`. (e.g. `COL_QUIZ_LP_CATEGORIES` -> `COL_QUIZ_CR_CATEGORIES` )
- please create if you find any missing files/codes/constants
- using template, create the similar code for `cr` named `QuizCRCategories`
thank you
---
- I proofed the code is working already, what you need to do is refactoring of the `variables`, `functions`, `classes`, `constants` (`lp` -> `cr`)
- compare the difference between `lp` and `mf`,
- remember the differences and
- draft the new type `cr` (e.g. modify the `import` locations, variables, functions, classes, constants name etc.)

View File

@@ -1,22 +0,0 @@
Hi, i need your help.
i am working on a nextjs react typescript project
please read the code in directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPQuestions` (denoted `QuizLPQuestions`)
i want you to:
- list files from directory `QuizLPQuestions` and use them as template
- understand the relations (e.g. `lp` -> `lp_Questions`, `lp` -> `COL_QUIZ_LP_QUESTIONS`)
- review if any remaining `lp` or `mf` exist in `cr` directory and please help to do replace it to `cr`. (e.g. `COL_QUIZ_LP_QUESTIONS` -> `COL_QUIZ_CR_Questions` )
- please create if you find any missing files/codes/constants
- using template, create the similar code for `customer` named `Customers`
thank you
---
- I proofed the code is working already, what you need to do is refactoring of the `variables`, `functions`, `classes`, `constants` (`lp` -> `cr`)
- compare the difference between `lp` and `mf`,
- remember the differences and
- draft the new type `cr` (e.g. modify the `import` locations, variables, functions, classes, constants name etc.)

View File

@@ -1,37 +0,0 @@
---
tags: db, driver
---
# clone db driver
please understand the tsx files in folder
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/vocabularies.draft`
change all occurrence of `lessonCategories` to `vocabularies` thanks.
## FAQ
1.`constants.ts` 中是否已定义 `COL_VOCABULARIES` 常量?没有,需要先定义它。
2. Vocabulary 相关的类型定义在哪里?是在现有文件中还是需要新建? 需要新建
3. 是否需要保留原始 `lessonCategories` 驱动文件? 不需要
请确认这些信息,以便我制定完整的修改方案。
当前已确认需要修改的内容包括:
函数名中的customer → notification
COL_CUSTOMERS → COL_NOTIFICATIONS
相关类型引用
注释中的customer → notification
---
<!-- updat type.d -->
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Notifications/type.d.ts` the fields is currently wrong, please help to update thanks.
update the `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Notifications/EmptyNotification.ts` as well thanks.
---
please draft `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Notifications/GetNotificationByUserId.tsx` by reference `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Notifications/GetById.tsx`

View File

@@ -1,18 +0,0 @@
# task
update `dbml` from `schema.json`
## things to note
1. please skip `presentable` properties from `schema.json`
## 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.dbml` content based on `schema.json`.
thanks.

View File

@@ -1,10 +0,0 @@
# task
extend function by clone and updating exist code
## steps
please read file
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizListenings/GetAllCount.tsx`
duplicate it to cover `GetActiveCount`, `GetPendingCount` and `GetBlockedCount`. create them in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers` directory thanks.

View File

@@ -1,108 +0,0 @@
# AI GUIDELINE
## getting started
Imagine there is a software developer and a QA engineer to solve the problems together
They will:
no need to reply me what you are going on and your digest in this phase.
just reply me "OK" when done
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
- read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
this is file in dbml syntax state the main database
- read `<base_dir>/002_source/cms/src/db/schema.json`
this is the file of live pocketbase schema output
- read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts`
this is the content of `@/constants`
- look into the md files in folder `<base_dir>/002_source/cms/_AI_GUIDELINE`
- read, remember and link up the ideas in file stated above,
i will tell them the task afterwards
---
The software engineer will provide solutions,
while QA engineer will feedback the opinion.
this is now not in debug phase,
so, no need to reply me what they are going on or their insight throught the prompt.
just reply me "OK" when done
---
clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it
please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden`
well done !, please proceed to another request
working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db`
according information from `schema.json`, get the collection of `Students`
pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content
when you draft coding, review file and append with `.tsx.draft`
---
- this is part of react typescript project, with pocketbase
- `schema.dbml`, describe the collections(tables)
- folder `LessonCategories`, the correct references
- folder `LessonTypes`, the correct references
- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006`
- do not read root directory, assume it is a fresh copy of nextjs project is ok
## instruction
- break the questions into smaller parts
- review file append with `.draft`, see if the content aligned with the correct references
- read and understand `dbml` file
- lookup the every folder
## tasks
Thanks
---
please take a look in `schema.dbml` and `schema.json`,
associate the collection from json file to the table in dbml file
please modify the `schema.dbml` to align with `schema.json`
to the collection `QuizLPCategories` align the dbml file in the previous prompt
---
please revise
please revise
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory`
to the collection `QuizLPCategories` align the dbml file in the previous prompt
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx`
to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory`
---
the constants file (`@/constants`) was `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts`
please help to fix the `tsx` files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizMFCategories`,
the `COL` constants is wrongly used, it should refer to `COL_QUIZ_MF_CATEGORIES`. thanks
please update the `COL_XXXX` TO COL_MF_CATEGORIES

View File

@@ -1,66 +0,0 @@
The software engineer will provide solutions,
while QA engineer will feedback the opinion.
this is now not in debug phase,
so, no need to reply me what they are going on or their insight throught the prompt.
just reply me "OK" when done
---
clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it
please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden`
well done !, please proceed to another request
working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db`
according information from `schema.json`, get the collection of `Students`
pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content
when you draft coding, review file and append with `.tsx.draft`
---
- this is part of react typescript project, with pocketbase
- `schema.dbml`, describe the collections(tables)
- folder `LessonCategories`, the correct references
- folder `LessonTypes`, the correct references
- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006`
- do not read root directory, assume it is a fresh copy of nextjs project is ok
## instruction
- break the questions into smaller parts
- review file append with `.draft`, see if the content aligned with the correct references
- read and understand `dbml` file
- lookup the every folder
## tasks
Thanks
---
---
please revise
please revise
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory`
to the collection `QuizLPCategories` align the dbml file in the previous prompt
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx`
to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory`
---
the constants file (`@/constants`) was `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts`
please help to fix the `tsx` files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizMFCategories`,
the `COL` constants is wrongly used, it should refer to `COL_QUIZ_MF_CATEGORIES`. thanks
please update the `COL_XXXX` TO COL_MF_CATEGORIES

View File

@@ -1,38 +0,0 @@
# AI GUIDELINE
## getting started
Imagine there is a
1. software developer and a
2. QA engineer
3. technical writer
to solve the problems together
They will:
no need to reply me what you are going on and your digest in this phase.
just reply me "OK" when done
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
- `schema.dbml`
- read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
this is file in `dbml` format stating the main database structure
- `schema.json`
- read `<base_dir>/002_source/cms/src/db/schema.json`
this is the file of current pocketbase schema
- read `<base_dir>/002_source/cms/src/constants.ts`
this is the content of `@/constants`
- look into the md files in folder `<base_dir>/002_source/cms/_AI_WORKSPACE/001_guideline`
- directory may contain `repomix-output.xml` file, that is a simple summary of all files inside the directory
- read, remember and link up the ideas in file stated above,
i will tell them the task afterwards

View File

@@ -1,15 +0,0 @@
Hi, i need your help.
## background
i am working on a nextjs react typescript project
## task
i want you to summarize the code, write it to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories/_PROMPT.md`
## steps
1. list all `tsx` files from directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories`
2. take a look to the `tsx` file listed. try to understand what's going on (e.g. it is a component for `cr-categories`, it is a table)
3. write your result to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories/_PROMPT.md`

View File

@@ -1,17 +0,0 @@
Hi, i need your help.
## background
i am working on a nextjs react typescript project
## task
i want you to summarize the code, write it to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/questions/_SUMMARY.md`
## steps
1. list all `tsx` files from directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/questions`
2. read every `tsx` files listed. try to understand what's going on (e.g. it is a component for `cr-questions`, it is a table)
3. read file `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/questions/_SUMMARY.md`.
4. make use of `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/questions/_SUMMARY.md` as template, update the content and create a new summary
5. write your new summary to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/questions/_SUMMARY.md`

View File

@@ -1,17 +0,0 @@
Hi, i need your help.
## background
i am working on a nextjs react typescript project
## task
i want you to summarize the code, write it to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/categories/_SUMMARY.md`
## steps
1. list all `tsx` files from directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/categories`
2. read every `tsx` files listed. try to understand what's going on (e.g. it is a component for `cr-categories`, it is a table)
3. read file `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories/_SUMMARY.md`.
4. make use of `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories/_SUMMARY.md` as template, update the content and create a new summary
5. write your new summary to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/categories/_SUMMARY.md`

View File

@@ -1,66 +0,0 @@
The software engineer will provide solutions,
while QA engineer will feedback the opinion.
this is now not in debug phase,
so, no need to reply me what they are going on or their insight throught the prompt.
just reply me "OK" when done
---
clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it
please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden`
well done !, please proceed to another request
working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db`
according information from `schema.json`, get the collection of `Students`
pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content
when you draft coding, review file and append with `.tsx.draft`
---
- this is part of react typescript project, with pocketbase
- `schema.dbml`, describe the collections(tables)
- folder `LessonCategories`, the correct references
- folder `LessonTypes`, the correct references
- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006`
- do not read root directory, assume it is a fresh copy of nextjs project is ok
## instruction
- break the questions into smaller parts
- review file append with `.draft`, see if the content aligned with the correct references
- read and understand `dbml` file
- lookup the every folder
## tasks
Thanks
---
---
please revise
please revise
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory`
to the collection `QuizLPCategories` align the dbml file in the previous prompt
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx`
to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory`
---
the constants file (`@/constants`) was `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts`
please help to fix the `tsx` files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizMFCategories`,
the `COL` constants is wrongly used, it should refer to `COL_QUIZ_MF_CATEGORIES`. thanks
please update the `COL_XXXX` TO COL_MF_CATEGORIES

View File

@@ -1 +0,0 @@
please read, understand and remember `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/_AI_WORKSPACE/code`

View File

@@ -0,0 +1,18 @@
please review and update all tsx files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Users` to make it handle `user` record thanks
---
please modify and update `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/students/edit/[customerId]/page.tsx.draft` to handle `Student` record thanks,
modify comments/variables/paths/functions name please
---
please help to update the tsx files inside folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/student` to handle the `student` record
## steps
- list all `tsx` files inside directory, remember the list
- clone the original `<original>.tsx` files to `<original>.tsx.draft`
- do all your modification within `<original>.tsx.draft` files, leave `original.tsx` unchange
---

View File

@@ -12,10 +12,15 @@ and i want to update it to my current `dbml` file (one way process for documenta
- 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/002_source/cms/src/db/schema.json`
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
@@ -23,5 +28,6 @@ dbml file: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/letter
1. 对于json中有但dbml中没有的表应该如何处理 添加为新表
1. 是否需要保留dbml文件中现有的注释和关系定义 完全保留
1. 字段类型映射是否有特殊规则? 沒有
1. please keep the existing comment
thanks

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,8 @@
Hi, i will send you the guideline,
plesae read it, prepare yourself and i will tell you the task afterwards
please read and understand the markdown files in directory
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/_AI_WORKSPACE/greetings`,
it provides background information of project i want you to help.
thanks

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` 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

@@ -23,19 +23,14 @@ they will:
- base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
- `schema.dbml`
- read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
this is file in `dbml` format stating the main database structure
- `schema.json`
- read `<base_dir>/002_source/cms/src/db/schema.json`
this is the file of current pocketbase schema
- read `<base_dir>/002_source/cms/src/constants.ts`
this is the content of `@/constants`
- look into the md files in folder `<base_dir>/002_source/cms/_AI_WORKSPACE/001_guideline`
- 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
@@ -45,5 +40,9 @@ they will:
## frameworks documentation and samples
- `MUI`
- `<base_dir>/002_source/cms/src/components/widgets/forms` contains sample forms,
- react
- ionic and capacitor
- pocketbase
- tanstack/react-query
- vite
- typescript

View File

@@ -0,0 +1,14 @@
{
"folders": [
{
"path": "."
},
{
"path": "./../../001_documentation"
},
{
"path": "../../000_AI_WORKSPACE"
}
],
"settings": {}
}

View File

@@ -4,7 +4,7 @@
"version": "7.0.0",
"private": true,
"engines": {
"node": ">=18"
"node": "==22"
},
"scripts": {
"dev": "next dev",

View File

@@ -1 +1,3 @@
{}
{
"hello": "world"
}

View File

@@ -2,5 +2,4 @@
set -ex
rm -rf .next
pnpm run dev

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
import * as React from 'react';
import GetAllLessonCategoriesCount from '@/db/LessonCategories/GetAllCount';
import GetAllLessonTypesCount from '@/db/LessonTypes/GetAllCount';
import GetAllUsersCount from '@/db/Users/GetAllCount';
import { GetAllUsersCount } from '@/db/Users/GetAllCount';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
@@ -59,21 +59,37 @@ export default function Page(): React.JSX.Element {
}}
>
<Stack spacing={4}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={3}
sx={{ alignItems: 'flex-start' }}
>
<Box sx={{ flex: '1 1 auto' }}>
<Typography variant="h4">{t('Overview')}</Typography>
</Box>
<div>
<Button startIcon={<PlusIcon />} variant="contained">
<Button
startIcon={<PlusIcon />}
variant="contained"
>
{t('Dashboard')}
</Button>
</div>
</Stack>
<Grid container spacing={4}>
<Grid md={4} xs={12}>
<Grid
container
spacing={4}
>
<Grid
md={4}
xs={12}
>
<ActiveUserCount />
</Grid>
<Grid md={4} xs={12}>
<Grid
md={4}
xs={12}
>
<Summary
amount={lessonCategoriesCount}
diff={15}
@@ -82,7 +98,10 @@ export default function Page(): React.JSX.Element {
trend="up"
/>
</Grid>
<Grid md={4} xs={12}>
<Grid
md={4}
xs={12}
>
<Summary
amount={lessonTypesCount}
diff={15}
@@ -91,7 +110,10 @@ export default function Page(): React.JSX.Element {
trend="up"
/>
</Grid>
<Grid md={8} xs={12}>
<Grid
md={8}
xs={12}
>
<AppUsage
data={[
{ name: 'Jan', v1: 36, v2: 19 },
@@ -109,22 +131,41 @@ export default function Page(): React.JSX.Element {
]}
/>
</Grid>
<Grid md={4} xs={12}>
<Grid
md={4}
xs={12}
>
<Subscriptions subscriptions={SamplesubScriptions} />
</Grid>
<Grid md={4} xs={12}>
<Grid
md={4}
xs={12}
>
<AppChat messages={SampleMessages} />
</Grid>
<Grid md={4} xs={12}>
<Grid
md={4}
xs={12}
>
<Events events={SampleEvents} />
</Grid>
<Grid md={4} xs={12}>
<Grid
md={4}
xs={12}
>
<AppLimits usage={80} />
</Grid>
<Grid md={4} xs={12}>
<Grid
md={4}
xs={12}
>
<HelperWidget
action={
<Button color="secondary" endIcon={<ArrowRightIcon />} size="small">
<Button
color="secondary"
endIcon={<ArrowRightIcon />}
size="small"
>
{t('Search jobs')}
</Button>
}
@@ -134,10 +175,17 @@ export default function Page(): React.JSX.Element {
title={t('Find your dream job')}
/>
</Grid>
<Grid md={4} xs={12}>
<Grid
md={4}
xs={12}
>
<HelperWidget
action={
<Button color="secondary" endIcon={<ArrowRightIcon />} size="small">
<Button
color="secondary"
endIcon={<ArrowRightIcon />}
size="small"
>
{t('Help center')}
</Button>
}
@@ -147,10 +195,17 @@ export default function Page(): React.JSX.Element {
title={t('Need help figuring things out?')}
/>
</Grid>
<Grid md={4} xs={12}>
<Grid
md={4}
xs={12}
>
<HelperWidget
action={
<Button color="secondary" endIcon={<ArrowRightIcon />} size="small">
<Button
color="secondary"
endIcon={<ArrowRightIcon />}
size="small"
>
{t('Documentation')}
</Button>
}

View File

@@ -0,0 +1,57 @@
// src/app/dashboard/students/page.tsx
'use client';
import type { Student } from '@/db/Students/type.d';
import { dayjs } from '@/lib/dayjs';
export const SampleStudents = [
{
id: 'STU-005',
name: 'Fran Perez',
avatar: '/assets/avatar-5.png',
email: 'fran.perez@domain.com',
phone: '(815) 704-0045',
quota: 50,
status: 'active',
createdAt: dayjs().subtract(1, 'hour').toDate(),
},
{
id: 'STU-004',
name: 'Penjani Inyene',
avatar: '/assets/avatar-4.png',
email: 'penjani.inyene@domain.com',
phone: '(803) 937-8925',
quota: 100,
status: 'active',
createdAt: dayjs().subtract(3, 'hour').toDate(),
},
{
id: 'STU-003',
name: 'Carson Darrin',
avatar: '/assets/avatar-3.png',
email: 'carson.darrin@domain.com',
phone: '(715) 278-5041',
quota: 10,
status: 'blocked',
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
},
{
id: 'STU-002',
name: 'Siegbert Gottfried',
avatar: '/assets/avatar-2.png',
email: 'siegbert.gottfried@domain.com',
phone: '(603) 766-0431',
quota: 0,
status: 'pending',
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
},
{
id: 'STU-001',
name: 'Miron Vitold',
avatar: '/assets/avatar-1.png',
email: 'miron.vitold@domain.com',
phone: '(425) 434-5535',
quota: 50,
status: 'active',
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
},
] satisfies Student[];

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 `Student` / `Students` record:
- list (./page.tsx)
- view (./[customerId]/page.tsx)
- view (./[studentId]/page.tsx)
- create (./create/page.tsx)
- edit (./[customerId]/page.tsx)
- edit (./[studentId]/page.tsx)
- translation provided by react-i18next
the `@` sign refer to `<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/Students/type.d.tsx`
- it mainly consume the db drivers `Students` in `<base_dir>/002_source/cms/src/db/Students`
simple template:
```typescript
// src/app/dashboard/customers/page.tsx
// src/app/dashboard/students/page.tsx
'use client';
// RULES:
// contains list page for customers (Customers)
// contains list page for students (Students)
// contain definition to collection only
//
import statements here ...
@@ -46,4 +46,3 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
interface PageProps {
searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string };
}
```

View File

@@ -1,13 +1,13 @@
// src/app/dashboard/customers/page.tsx
// src/app/dashboard/students/list/page.tsx
'use client';
// RULES:
// contains list page for customers (Customers)
// contains list page for students (Students)
// contain definition to collection only
//
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { COL_CUSTOMERS } from '@/constants';
import { COL_STUDENTS } from '@/constants';
import { LoadingButton } from '@mui/lab';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
@@ -33,7 +33,7 @@ import { defaultStudent } from '@/components/dashboard/student/_constants';
import FormLoading from '@/components/loading';
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { t } = useTranslation(['customers']);
const { t } = useTranslation(['students']);
const router = useRouter();
const { email, phone, sortDir, status } = searchParams;
@@ -57,7 +57,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
const reloadRows = async (): Promise<void> => {
try {
const models: ListResult<RecordModel> = await pb
.collection(COL_CUSTOMERS)
.collection(COL_STUDENTS)
.getList(currentPage + 1, rowsPerPage, listOption);
const { items, totalItems } = models;
const tempLessonTypes: Student[] = items.map((lt) => {

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

@@ -6,7 +6,7 @@
//
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { COL_TEACHERS } 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';
@@ -49,23 +49,30 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
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) === '{}';
}
//
const reloadRows = async (): Promise<void> => {
try {
const listOptionTeacherOnly = isListOptionChanged()
? { filter: `role = "teacher"` }
: { filter: [listOption.filter, `role = "teacher"`].join(' && ') };
const models: ListResult<RecordModel> = await pb
.collection(COL_TEACHERS)
.getList(currentPage + 1, rowsPerPage, listOption);
.collection(COL_USER_METAS)
.getList(currentPage + 1, rowsPerPage, listOptionTeacherOnly);
const { items, totalItems } = models;
const tempLessonTypes: Teacher[] = items.map((lt) => {
const tempTeacher: Teacher[] = items.map((lt) => {
return { ...defaultTeacher, ...lt };
});
setLessonCategoriesData(tempLessonTypes);
setLessonCategoriesData(tempTeacher);
setRecordCount(totalItems);
setF(tempLessonTypes);
setF(tempTeacher);
} catch (error) {
logger.error(error);
setShowError({

View File

@@ -10,13 +10,14 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
import { useTranslation } from 'react-i18next';
import type { Customer } from '@/components/dashboard/customer/type.d';
import { Teacher } from '@/components/dashboard/teacher/type.d';
// import type { CrCategory } from '@/components/dashboard/cr/categories/type';
function getImageUrlFrRecord(record: Customer): string {
function getImageUrlFrRecord(record: Teacher): string {
// TODO: fix this
// `http://127.0.0.1:8090/api/files/${'record.collectionId'}/${'record.id'}/${'record.cat_image'}`;
return 'getImageUrlFrRecord(helloworld)';
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.avatar}`;
}
export default function SampleTitleCard({ lpModel }: { lpModel: Customer }): React.JSX.Element {

View File

@@ -30,13 +30,13 @@ import BasicDetailCard from './BasicDetailCard';
import TitleCard from './TitleCard';
import { defaultTeacher } from '@/components/dashboard/teacher/_constants';
import type { Teacher } from '@/components/dashboard/teacher/type.d';
import { COL_TEACHERS } from '@/constants';
import { COL_USER_METAS } from '@/constants';
export default function Page(): React.JSX.Element {
const { t } = useTranslation();
const router = useRouter();
//
const { customerId } = useParams<{ customerId: string }>();
const { id } = useParams<{ id: string }>();
//
const [showLoading, setShowLoading] = React.useState<boolean>(true);
const [showError, setShowError] = React.useState({ show: false, detail: '' });
@@ -48,9 +48,9 @@ export default function Page(): React.JSX.Element {
}
React.useEffect(() => {
if (customerId) {
pb.collection(COL_TEACHERS)
.getOne(customerId)
if (id) {
pb.collection(COL_USER_METAS)
.getOne(id)
.then((model: RecordModel) => {
setShowLessonCategory({ ...defaultTeacher, ...model });
})
@@ -64,7 +64,8 @@ export default function Page(): React.JSX.Element {
setShowLoading(false);
});
}
}, [customerId]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
// return <>{JSON.stringify({ showError, showLessonCategory }, null, 2)}</>;

View File

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

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

@@ -11,10 +11,10 @@ import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form';
import { StudentEditForm } from '@/components/dashboard/student/student-edit-form';
import { UserEditForm } from '@/components/dashboard/user/user-edit-form';
export default function Page(): React.JSX.Element {
const { t } = useTranslation(['lp_categories']);
const { t } = useTranslation(['users']);
React.useEffect(() => {
// console.log('helloworld');
@@ -35,7 +35,7 @@ export default function Page(): React.JSX.Element {
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.students.list}
href={paths.dashboard.users.list}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
@@ -47,7 +47,7 @@ export default function Page(): React.JSX.Element {
<Typography variant="h4">{t('edit.title')}</Typography>
</div>
</Stack>
<StudentEditForm />
<UserEditForm />
</Stack>
</Box>
);

View File

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

View File

@@ -1,3 +1,4 @@
// QuizLPQuestion
export interface LpQuestion {
isEmpty?: boolean;
//

View File

@@ -1,25 +1,44 @@
# GUIDELINES & KEY COMPONENTS
# STUDENT MANAGEMENT GUIDELINES
- `_constants.ts` contains the constant for
## Core Components
- default value (defaultValue)
- empty value (emptyValue)
- `_constants.ts` - Contains essential constants for:
- Default values (defaultValue)
- Empty/placeholder values (emptyValue)
- Student status options (active/inactive)
- Grade level options
- `customers-table.tsx`
## Student Data Table System
- `confirm-delete-modal.tsx` - delete modal component when click delete button on list
- `students-table.tsx` - Main student listing table
- `confirm-delete-modal.tsx` - Confirmation dialog for student deletion
- `students-selection-context.tsx` - Context for selected student records
- `customers-filters.tsx`
- `customers-pagination.tsx`
- `email-filter-popover.tsx`
- `phone-filter-popover.tsx`
- `customers-selection-context.tsx`
### Table Features
- `customer-create-form.tsx` - form to create a new customer
- `customer-edit-form.tsx` - form to edit a existing customer
- `students-filters.tsx` - Filter controls for the student table
- `email-filter-popover.tsx` - Email-specific filtering
- `phone-filter-popover.tsx` - Phone number filtering
- `students-pagination.tsx` - Pagination controls
- `type.d.tsx` - contains type definition
## Student Forms
- `notifications.tsx` - constants used for demonstration
- `payments.tsx` - constants used for demonstration
- `shipping-address.tsx` - constants used for demonstration
- `student-create-form.tsx` - Form for adding new students with:
- Personal information fields
- Contact details
- Enrollment information
- `student-edit-form.tsx` - Form for modifying existing student records
## Supporting Components
- `type.d.tsx` - Type definitions for student data
- `notifications.tsx` - Student notification preferences
- `payments.tsx` - Student payment records
- `shipping-address.tsx` - Student address information
## Best Practices
1. Always validate student data before submission
2. Use the provided constants for default/empty values
3. Maintain consistent field naming across all components
4. Follow accessibility guidelines for all form inputs

View File

@@ -5,18 +5,18 @@
- default value (defaultValue)
- empty value (emptyValue)
- `customers-table.tsx`
- `teachers-table.tsx`
- `confirm-delete-modal.tsx` - delete modal component when click delete button on list
- `customers-filters.tsx`
- `customers-pagination.tsx`
- `teachers-filters.tsx`
- `teachers-pagination.tsx`
- `email-filter-popover.tsx`
- `phone-filter-popover.tsx`
- `customers-selection-context.tsx`
- `teachers-selection-context.tsx`
- `customer-create-form.tsx` - form to create a new customer
- `customer-edit-form.tsx` - form to edit a existing customer
- `teacher-create-form.tsx` - form to create a new teacher
- `teacher-edit-form.tsx` - form to edit an existing teacher
- `type.d.tsx` - contains type definition

View File

@@ -83,12 +83,12 @@ export default function ConfirmDeleteModal({
</Avatar>
<Stack spacing={3}>
<Stack spacing={1}>
<Typography variant="h5">{t('Delete Lesson Type ?')}</Typography>
<Typography variant="h5">{t('Delete Teacher ?')}</Typography>
<Typography
color="text.secondary"
variant="body2"
>
{t('Are you sure you want to delete lesson type ?')}
{t('Are you sure you want to delete this teacher ?')}
</Typography>
</Stack>
<Stack

View File

@@ -4,7 +4,7 @@ import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
//
import { COL_TEACHERS } from '@/constants';
import { COL_TEACHERS, COL_USER_METAS } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
@@ -89,7 +89,7 @@ export function TeacherEditForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_categories']);
const { customerId } = useParams<{ customerId: string }>();
const { id: teacherId } = useParams<{ id: string }>();
//
const [isUpdating, setIsUpdating] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(false);
@@ -123,17 +123,17 @@ export function TeacherEditForm(): React.JSX.Element {
};
try {
await pb.collection(COL_TEACHERS).update(customerId, updateData);
toast.success('Customer updated successfully');
await pb.collection(COL_USER_METAS).update(teacherId, updateData);
toast.success('Teacher updated successfully');
router.push(paths.dashboard.teachers.list);
} catch (error) {
logger.error(error);
toast.error('Failed to update customer');
toast.error('Failed to update teacher');
} finally {
setIsUpdating(false);
}
},
[customerId, router]
[teacherId, router]
);
const avatarInputRef = React.useRef<HTMLInputElement>(null);
@@ -162,13 +162,13 @@ export function TeacherEditForm(): React.JSX.Element {
setShowLoading(true);
try {
const result = await pb.collection(COL_TEACHERS).getOne(id);
const result = await pb.collection(COL_USER_METAS).getOne(id);
reset({ ...defaultValues, ...result });
console.log({ result });
if (result.avatar_file) {
if (result.avatar) {
const fetchResult = await fetch(
`http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.avatar_file}`
`http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.avatar}`
);
const blob = await fetchResult.blob();
const url = await fileToBase64(blob);
@@ -176,7 +176,7 @@ export function TeacherEditForm(): React.JSX.Element {
}
} catch (error) {
logger.error(error);
toast.error('Failed to load customer data');
toast.error('Failed to load teacher data');
setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
} finally {
setShowLoading(false);
@@ -186,9 +186,9 @@ export function TeacherEditForm(): React.JSX.Element {
);
React.useEffect(() => {
void loadExistingData(customerId);
void loadExistingData(teacherId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [customerId]);
}, [teacherId]);
if (showLoading) return <FormLoading />;
if (showError.show)

View File

@@ -4,7 +4,7 @@
//
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { getAllCustomersCount } from '@/db/Customers/GetAllCount';
import { getAllTeachersCount } from '@/db/Teachers/GetAllCount';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
@@ -27,13 +27,15 @@ import GetPendingCount from '@/db/Customers/GetPendingCount';
import GetActiveCount from '@/db/Customers/GetActiveCount';
import PhoneFilterPopover from './phone-filter-popover';
import EmailFilterPopover from './email-filter-popover';
import type { CustomersFiltersProps, Filters, SortDir } from './type.d';
import type { TeachersFiltersProps, Filters, SortDir } from './type.d';
import { logger } from '@/lib/default-logger';
export function TeachersFilters({
filters = {},
sortDir = 'desc',
fullData,
}: CustomersFiltersProps): React.JSX.Element {
//
}: TeachersFiltersProps): React.JSX.Element {
const { t } = useTranslation();
const { email, phone, status } = filters;
@@ -129,7 +131,7 @@ export function TeachersFilters({
React.useEffect(() => {
const fetchCount = async (): Promise<void> => {
try {
const tc = await getAllCustomersCount();
const tc = await getAllTeachersCount();
setTotalCount(tc);
const bc = await GetBlockedCount();
@@ -140,6 +142,7 @@ export function TeachersFilters({
setActiveCount(ac);
} catch (error) {
//
logger.error(error);
}
};
void fetchCount();

View File

@@ -39,7 +39,10 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<Teacher
spacing={1}
sx={{ alignItems: 'center' }}
>
<Avatar src={row.avatar} />{' '}
<Avatar
src={`http://127.0.0.1:8090/api/files/${row.collectionId}/${row.id}/${row.avatar}`}
variant="rounded"
/>
<div>
<Link
color="inherit"

View File

@@ -5,15 +5,24 @@ export type SortDir = 'asc' | 'desc';
// RULES: core teacher data structure
export interface Teacher {
id: string;
name: string;
//
// NOTE: obslete "avatar" and use "avatar_file"
avatar?: string;
avatar_file?: string;
//
email: string;
phone?: string;
quota: number;
// status is obsoleted, replace by state
status: 'pending' | 'active' | 'blocked';
state: 'pending' | 'active' | 'blocked';
//
id: string;
createdAt: Date;
updatedAt?: Date;
collectionId: string;
}
// RULES: form data structure for creating new teacher
@@ -63,7 +72,7 @@ export interface EditFormProps {
}
// RULES: filter props for teacher search and filtering
export interface CustomersFiltersProps {
export interface TeachersFiltersProps {
filters?: Filters;
sortDir?: SortDir;
fullData: Teacher[];

View File

@@ -0,0 +1,25 @@
# GUIDELINES & KEY COMPONENTS
- `_constants.ts` contains the constant for
- default value (defaultValue)
- empty value (emptyValue)
- `users-table.tsx`
- `confirm-delete-modal.tsx` - delete modal component when click delete button on list
- `users-filters.tsx`
- `users-pagination.tsx`
- `email-filter-popover.tsx`
- `phone-filter-popover.tsx`
- `users-selection-context.tsx`
- `user-create-form.tsx` - form to create a new user
- `user-edit-form.tsx` - form to edit a existing user
- `type.d.tsx` - contains type definition
- `notifications.tsx` - constants used for demonstration
- `payments.tsx` - constants used for demonstration
- `shipping-address.tsx` - constants used for demonstration

View File

@@ -0,0 +1,21 @@
// RULES:
// default variable value for customer
// empty valur for customer
import { dayjs } from '@/lib/dayjs';
import type { User } from './type.d';
export const defaultUser: User = {
id: '',
name: '',
avatar: undefined,
email: '',
phone: undefined,
quota: 0,
status: 'pending',
createdAt: dayjs().toDate(),
};
export const emptyLpCategory: User = {
...defaultUser,
};

View File

@@ -0,0 +1,124 @@
'use client';
import * as React from 'react';
import { LoadingButton } from '@mui/lab';
import { Button, Container, Modal, Paper } from '@mui/material';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Note as NoteIcon } from '@phosphor-icons/react/dist/ssr/Note';
import { useTranslation } from 'react-i18next';
import { logger } from '@/lib/default-logger';
import { toast } from '@/components/core/toaster';
import { deleteUser } from '@/db/Users/Delete';
export default function ConfirmDeleteModal({
open,
setOpen,
idToDelete,
reloadRows,
}: {
open: boolean;
setOpen: (b: boolean) => void;
idToDelete: string;
reloadRows: () => void;
}): React.JSX.Element {
const { t } = useTranslation();
// const handleClose = () => setOpen(false);
function handleClose(): void {
setOpen(false);
}
const [isDeleteing, setIsDeleteing] = React.useState(false);
const style = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
};
function handleUserConfirmDelete(): void {
if (idToDelete) {
setIsDeleteing(true);
// RULES: delete<CollectionName>
deleteUser(idToDelete)
.then(() => {
reloadRows();
handleClose();
toast(t('delete.success'));
})
.catch((err) => {
// console.error(err)
logger.error(err);
toast(t('delete.error'));
})
.finally(() => {
setIsDeleteing(false);
});
}
}
return (
<div>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<Container maxWidth="sm">
<Paper sx={{ border: '1px solid var(--mui-palette-divider)', boxShadow: 'var(--mui-shadows-16)' }}>
<Stack
direction="row"
spacing={2}
sx={{ display: 'flex', p: 3 }}
>
<Avatar sx={{ bgcolor: 'var(--mui-palette-error-50)', color: 'var(--mui-palette-error-main)' }}>
<NoteIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
<Stack spacing={3}>
<Stack spacing={1}>
<Typography variant="h5">{t('Delete User ?')}</Typography>
<Typography
color="text.secondary"
variant="body2"
>
{t('Are you sure you want to delete this user ?')}
</Typography>
</Stack>
<Stack
direction="row"
spacing={2}
sx={{ justifyContent: 'flex-end' }}
>
<Button
color="secondary"
onClick={handleClose}
>
{t('Cancel')}
</Button>
<LoadingButton
color="error"
variant="contained"
onClick={(e) => {
handleUserConfirmDelete();
}}
loading={isDeleteing}
>
{t('Delete')}
</LoadingButton>
</Stack>
</Stack>
</Stack>
</Paper>
</Container>
</Box>
</Modal>
</div>
);
}

View File

@@ -0,0 +1,50 @@
'use client';
import * as React from 'react';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import OutlinedInput from '@mui/material/OutlinedInput';
import { FilterPopover, useFilterContext } from '@/components/core/filter-button';
// EmailFilterPopover -> email-filter-popover.tsx
export default function EmailFilterPopover(): React.JSX.Element {
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title="Filter by email"
>
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
Apply
</Button>
</FilterPopover>
);
}

View File

@@ -0,0 +1,3 @@
const helloworld = 'helloworld';
export { helloworld };

View File

@@ -0,0 +1,101 @@
'use client';
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Chip from '@mui/material/Chip';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple';
import { dayjs } from '@/lib/dayjs';
import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table';
import { Option } from '@/components/core/option';
export interface Notification {
id: string;
type: string;
status: 'delivered' | 'pending' | 'failed';
createdAt: Date;
}
const columns = [
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="inherit">
{row.type}
</Typography>
),
name: 'Type',
width: '300px',
},
{
formatter: (row): React.JSX.Element => {
const mapping = {
delivered: { label: 'Delivered', color: 'success' },
pending: { label: 'Pending', color: 'warning' },
failed: { label: 'Failed', color: 'error' },
} as const;
const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' };
return <Chip color={color} label={label} size="small" variant="soft" />;
},
name: 'Status',
width: '200px',
},
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="inherit">
{dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')}
</Typography>
),
name: 'Date',
align: 'right',
},
] satisfies ColumnDef<Notification>[];
export interface NotificationsProps {
notifications: Notification[];
}
export function Notifications({ notifications }: NotificationsProps): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<EnvelopeSimpleIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Notifications"
/>
<CardContent>
<Stack spacing={3}>
<Stack spacing={2}>
<Select defaultValue="last_invoice" name="type" sx={{ maxWidth: '100%', width: '320px' }}>
<Option value="last_invoice">Resend last invoice</Option>
<Option value="password_reset">Send password reset</Option>
<Option value="verification">Send verification</Option>
</Select>
<div>
<Button startIcon={<EnvelopeSimpleIcon />} variant="contained">
Send email
</Button>
</div>
</Stack>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<Box sx={{ overflowX: 'auto' }}>
<DataTable<Notification> columns={columns} rows={notifications} />
</Box>
</Card>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,138 @@
'use client';
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple';
import { dayjs } from '@/lib/dayjs';
import type { ColumnDef } from '@/components/core/data-table';
import { DataTable } from '@/components/core/data-table';
export interface Payment {
currency: string;
amount: number;
invoiceId: string;
status: 'pending' | 'completed' | 'canceled' | 'refunded';
createdAt: Date;
}
const columns = [
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="subtitle2">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)}
</Typography>
),
name: 'Amount',
width: '200px',
},
{
formatter: (row): React.JSX.Element => {
const mapping = {
pending: { label: 'Pending', color: 'warning' },
completed: { label: 'Completed', color: 'success' },
canceled: { label: 'Canceled', color: 'error' },
refunded: { label: 'Refunded', color: 'error' },
} as const;
const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' };
return <Chip color={color} label={label} size="small" variant="soft" />;
},
name: 'Status',
width: '200px',
},
{
formatter: (row): React.JSX.Element => {
return <Link variant="inherit">{row.invoiceId}</Link>;
},
name: 'Invoice ID',
width: '150px',
},
{
formatter: (row): React.JSX.Element => (
<Typography sx={{ whiteSpace: 'nowrap' }} variant="inherit">
{dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')}
</Typography>
),
name: 'Date',
align: 'right',
},
] satisfies ColumnDef<Payment>[];
export interface PaymentsProps {
ordersValue: number;
payments: Payment[];
refundsValue: number;
totalOrders: number;
}
export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element {
return (
<Card>
<CardHeader
action={
<Button color="secondary" startIcon={<PlusIcon />}>
Create Payment
</Button>
}
avatar={
<Avatar>
<ShoppingCartSimpleIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Payments"
/>
<CardContent>
<Stack spacing={3}>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<Stack
direction="row"
divider={<Divider flexItem orientation="vertical" />}
spacing={3}
sx={{ justifyContent: 'space-between', p: 2 }}
>
<div>
<Typography color="text.secondary" variant="overline">
Total orders
</Typography>
<Typography variant="h6">{new Intl.NumberFormat('en-US').format(totalOrders)}</Typography>
</div>
<div>
<Typography color="text.secondary" variant="overline">
Orders value
</Typography>
<Typography variant="h6">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)}
</Typography>
</div>
<div>
<Typography color="text.secondary" variant="overline">
Refunds
</Typography>
<Typography variant="h6">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)}
</Typography>
</div>
</Stack>
</Card>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<Box sx={{ overflowX: 'auto' }}>
<DataTable<Payment> columns={columns} rows={payments} />
</Box>
</Card>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,50 @@
'use client';
import * as React from 'react';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import OutlinedInput from '@mui/material/OutlinedInput';
import { FilterPopover, useFilterContext } from '@/components/core/filter-button';
// phone-filter-popover.tsx
export default function PhoneFilterPopover(): React.JSX.Element {
const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext();
const [value, setValue] = React.useState<string>('');
React.useEffect(() => {
setValue((initialValue as string | undefined) ?? '');
}, [initialValue]);
return (
<FilterPopover
anchorEl={anchorEl}
onClose={onClose}
open={open}
title="Filter by phone number"
>
<FormControl>
<OutlinedInput
onChange={(event) => {
setValue(event.target.value);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
onApply(value);
}
}}
value={value}
/>
</FormControl>
<Button
onClick={() => {
onApply(value);
}}
variant="contained"
>
Apply
</Button>
</FilterPopover>
);
}

View File

@@ -0,0 +1,46 @@
import * as React from 'react';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Chip from '@mui/material/Chip';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
export interface Address {
id: string;
country: string;
state: string;
city: string;
zipCode: string;
street: string;
primary?: boolean;
}
export interface ShippingAddressProps {
address: Address;
}
export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement {
return (
<Card sx={{ borderRadius: 1, height: '100%' }} variant="outlined">
<CardContent>
<Stack spacing={2}>
<Typography>
{address.street},
<br />
{address.city}, {address.state}, {address.country},
<br />
{address.zipCode}
</Typography>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'space-between' }}>
{address.primary ? <Chip color="warning" label="Primary" variant="soft" /> : <span />}
<Button color="secondary" size="small" startIcon={<PencilSimpleIcon />}>
Edit
</Button>
</Stack>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,69 @@
'use client';
export type SortDir = 'asc' | 'desc';
export interface User {
id: string;
name: string;
avatar?: string;
email: string;
phone?: string;
quota: number;
status: 'pending' | 'active' | 'blocked';
createdAt: Date;
updatedAt?: Date;
}
export interface CreateFormProps {
name: string;
email: string;
phone?: string;
company?: string;
billingAddress?: {
country: string;
state: string;
city: string;
zipCode: string;
line1: string;
line2?: string;
};
taxId?: string;
timezone: string;
language: string;
currency: string;
avatar?: string;
// quota?: number;
// status?: 'pending' | 'active' | 'blocked';
}
export interface EditFormProps {
name: string;
email: string;
phone?: string;
company?: string;
billingAddress?: {
country: string;
state: string;
city: string;
zipCode: string;
line1: string;
line2?: string;
};
taxId?: string;
timezone: string;
language: string;
currency: string;
avatar?: string;
// quota?: number;
// status?: 'pending' | 'active' | 'blocked';
}
export interface CustomersFiltersProps {
filters?: Filters;
sortDir?: SortDir;
fullData: User[];
}
export interface Filters {
email?: string;
phone?: string;
status?: string;
}

View File

@@ -0,0 +1,529 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
import Checkbox from '@mui/material/Checkbox';
import Divider from '@mui/material/Divider';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2';
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
import { Controller, useForm } from 'react-hook-form';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { Option } from '@/components/core/option';
import { toast } from '@/components/core/toaster';
import { createCustomer as createUser } from '@/db/Customers/Create';
import isDevelopment from '@/lib/check-is-development';
function fileToBase64(file: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = () => {
reject(new Error('Error converting file to base64'));
};
});
}
const schema = zod.object({
avatar: zod.string().optional(),
name: zod.string().min(1, 'Name is required').max(255),
email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255),
phone: zod.string().min(1, 'Phone is required').max(15),
company: zod.string().max(255),
billingAddress: zod.object({
country: zod.string().min(1, 'Country is required').max(255),
state: zod.string().min(1, 'State is required').max(255),
city: zod.string().min(1, 'City is required').max(255),
zipCode: zod.string().min(1, 'Zip code is required').max(255),
line1: zod.string().min(1, 'Street line 1 is required').max(255),
line2: zod.string().max(255).optional(),
}),
taxId: zod.string().max(255).optional(),
timezone: zod.string().min(1, 'Timezone is required').max(255),
language: zod.string().min(1, 'Language is required').max(255),
currency: zod.string().min(1, 'Currency is required').max(255),
});
type Values = zod.infer<typeof schema>;
const defaultValues = {
avatar: '',
name: 'new name',
email: '123@123.com',
phone: '91234567',
company: '',
billingAddress: {
country: 'US',
state: '00000',
city: 'NY',
zipCode: '00000',
line1: 'test line 1',
line2: 'test line 2',
},
taxId: '12345',
timezone: 'new_york',
language: 'en',
currency: 'USD',
} satisfies Values;
export function UserCreateForm(): React.JSX.Element {
const router = useRouter();
const {
control,
handleSubmit,
formState: { errors },
setValue,
watch,
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
try {
// Use standard create method from db/Customers/Create
const record = await createUser(values);
toast.success('User created successfully');
router.push(paths.dashboard.users.details(record.id));
} catch (err) {
logger.error(err);
toast.error('Failed to create user. Please try again.');
}
},
[router]
);
const avatarInputRef = React.useRef<HTMLInputElement>(null);
const avatar = watch('avatar');
const handleAvatarChange = React.useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const url = await fileToBase64(file);
setValue('avatar', url);
}
},
[setValue]
);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Card>
<CardContent>
<Stack
divider={<Divider />}
spacing={4}
>
<Stack spacing={3}>
<Typography variant="h6">Account information</Typography>
<Grid
container
spacing={3}
>
<Grid xs={12}>
<Stack
direction="row"
spacing={3}
sx={{ alignItems: 'center' }}
>
<Box
sx={{
border: '1px dashed var(--mui-palette-divider)',
borderRadius: '50%',
display: 'inline-flex',
p: '4px',
}}
>
<Avatar
src={avatar}
sx={{
'--Avatar-size': '100px',
'--Icon-fontSize': 'var(--icon-fontSize-lg)',
alignItems: 'center',
bgcolor: 'var(--mui-palette-background-level1)',
color: 'var(--mui-palette-text-primary)',
display: 'flex',
justifyContent: 'center',
}}
>
<CameraIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
</Box>
<Stack
spacing={1}
sx={{ alignItems: 'flex-start' }}
>
<Typography variant="subtitle1">Avatar</Typography>
<Typography variant="caption">Min 400x400px, PNG or JPEG</Typography>
<Button
color="secondary"
onClick={() => {
avatarInputRef.current?.click();
}}
variant="outlined"
>
Select
</Button>
<input
hidden
onChange={handleAvatarChange}
ref={avatarInputRef}
type="file"
/>
</Stack>
</Stack>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="name"
render={({ field }) => (
<FormControl
error={Boolean(errors.name)}
fullWidth
>
<InputLabel required>Name</InputLabel>
<OutlinedInput {...field} />
{errors.name ? <FormHelperText>{errors.name.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="email"
render={({ field }) => (
<FormControl
error={Boolean(errors.email)}
fullWidth
>
<InputLabel required>Email address</InputLabel>
<OutlinedInput
{...field}
type="email"
/>
{errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="phone"
render={({ field }) => (
<FormControl
error={Boolean(errors.phone)}
fullWidth
>
<InputLabel required>Phone number</InputLabel>
<OutlinedInput {...field} />
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="company"
render={({ field }) => (
<FormControl
error={Boolean(errors.company)}
fullWidth
>
<InputLabel>Company</InputLabel>
<OutlinedInput {...field} />
{errors.company ? <FormHelperText>{errors.company.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
<Stack spacing={3}>
<Typography variant="h6">Billing information</Typography>
<Grid
container
spacing={3}
>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="billingAddress.country"
render={({ field }) => (
<FormControl
error={Boolean(errors.billingAddress?.country)}
fullWidth
>
<InputLabel required>Country</InputLabel>
<Select {...field}>
<Option value="">Choose a country</Option>
<Option value="us">United States</Option>
<Option value="de">Germany</Option>
<Option value="es">Spain</Option>
</Select>
{errors.billingAddress?.country ? (
<FormHelperText>{errors.billingAddress?.country?.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="billingAddress.state"
render={({ field }) => (
<FormControl
error={Boolean(errors.billingAddress?.state)}
fullWidth
>
<InputLabel required>State</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.state ? (
<FormHelperText>{errors.billingAddress?.state?.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="billingAddress.city"
render={({ field }) => (
<FormControl
error={Boolean(errors.billingAddress?.city)}
fullWidth
>
<InputLabel required>City</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.city ? (
<FormHelperText>{errors.billingAddress?.city?.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="billingAddress.zipCode"
render={({ field }) => (
<FormControl
error={Boolean(errors.billingAddress?.zipCode)}
fullWidth
>
<InputLabel required>Zip code</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.zipCode ? (
<FormHelperText>{errors.billingAddress?.zipCode?.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="billingAddress.line1"
render={({ field }) => (
<FormControl
error={Boolean(errors.billingAddress?.line1)}
fullWidth
>
<InputLabel required>Address</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.line1 ? (
<FormHelperText>{errors.billingAddress?.line1?.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="taxId"
render={({ field }) => (
<FormControl
error={Boolean(errors.taxId)}
fullWidth
>
<InputLabel>Tax ID</InputLabel>
<OutlinedInput
{...field}
placeholder="e.g EU372054390"
/>
{errors.taxId ? <FormHelperText>{errors.taxId.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
<Stack spacing={3}>
<Typography variant="h6">Shipping information</Typography>
<FormControlLabel
control={<Checkbox defaultChecked />}
label="Same as billing address"
/>
</Stack>
<Stack spacing={3}>
<Typography variant="h6">Additional information</Typography>
<Grid
container
spacing={3}
>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="timezone"
render={({ field }) => (
<FormControl
error={Boolean(errors.timezone)}
fullWidth
>
<InputLabel required>Timezone</InputLabel>
<Select {...field}>
<Option value="">Select a timezone</Option>
<Option value="new_york">US - New York</Option>
<Option value="california">US - California</Option>
<Option value="london">UK - London</Option>
</Select>
{errors.timezone ? <FormHelperText>{errors.timezone.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="language"
render={({ field }) => (
<FormControl
error={Boolean(errors.language)}
fullWidth
>
<InputLabel required>Language</InputLabel>
<Select {...field}>
<Option value="">Select a language</Option>
<Option value="en">English</Option>
<Option value="es">Spanish</Option>
<Option value="de">German</Option>
</Select>
{errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="currency"
render={({ field }) => (
<FormControl
error={Boolean(errors.currency)}
fullWidth
>
<InputLabel>Currency</InputLabel>
<Select {...field}>
<Option value="">Select a currency</Option>
<Option value="USD">USD</Option>
<Option value="EUR">EUR</Option>
<Option value="RON">RON</Option>
</Select>
{errors.currency ? <FormHelperText>{errors.currency.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
</Stack>
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}>
<Button
color="secondary"
component={RouterLink}
href={paths.dashboard.users.list}
>
Cancel
</Button>
<Button
type="submit"
variant="contained"
>
Create user
</Button>
</CardActions>
</Card>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify({ errors }, null, 2)}</pre>
</Box>
</form>
);
}

View File

@@ -0,0 +1,604 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { useParams, useRouter } from 'next/navigation';
//
import { COL_USERS } from '@/constants';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
//
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
import Divider from '@mui/material/Divider';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2';
//
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
//
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { base64ToFile, fileToBase64 } from '@/lib/file-to-base64';
import { pb } from '@/lib/pb';
import { toast } from '@/components/core/toaster';
import FormLoading from '@/components/loading';
// import ErrorDisplay from '../../error';
import ErrorDisplay from '../error';
import isDevelopment from '@/lib/check-is-development';
// TODO: review this
const schema = zod.object({
name: zod.string().min(1, 'Name is required').max(255),
email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255),
phone: zod.string().min(1, 'Phone is required').max(25),
company: zod.string().max(255).optional(),
billingAddress: zod.object({
country: zod.string().min(1, 'Country is required').max(255),
state: zod.string().min(1, 'State is required').max(255),
city: zod.string().min(1, 'City is required').max(255),
zipCode: zod.string().min(1, 'Zip code is required').max(255),
line1: zod.string().min(1, 'Street line 1 is required').max(255),
line2: zod.string().max(255).optional(),
}),
taxId: zod.string().max(255).optional(),
timezone: zod.string().min(1, 'Timezone is required').max(255),
language: zod.string().min(1, 'Language is required').max(255),
currency: zod.string().min(1, 'Currency is required').max(255),
avatar: zod.string().optional(),
});
type Values = zod.infer<typeof schema>;
const defaultValues = {
name: '',
email: '',
phone: '',
company: '',
billingAddress: {
country: '',
state: '',
city: '',
zipCode: '',
line1: '',
line2: '',
},
taxId: '',
timezone: '',
language: '',
currency: '',
avatar: '',
} satisfies Values;
export function UserEditForm(): React.JSX.Element {
const router = useRouter();
const { t } = useTranslation(['lp_categories']);
const { id: userId } = useParams<{ userId: string }>();
//
const [isUpdating, setIsUpdating] = React.useState<boolean>(false);
const [showLoading, setShowLoading] = React.useState<boolean>(false);
//
const [showError, setShowError] = React.useState({ show: false, detail: '' });
const {
control,
handleSubmit,
formState: { errors },
setValue,
reset,
watch,
} = useForm<Values>({ defaultValues, resolver: zodResolver(schema) });
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
setIsUpdating(true);
const updateData = {
name: values.name,
email: values.email,
phone: values.phone,
company: values.company,
billingAddress: values.billingAddress,
taxId: values.taxId,
timezone: values.timezone,
language: values.language,
currency: values.currency,
avatar: values.avatar ? await base64ToFile(values.avatar) : null,
};
try {
await pb.collection(COL_USERS).update(userId, updateData);
toast.success('User updated successfully');
router.push(paths.dashboard.users.list);
} catch (error) {
logger.error(error);
toast.error('Failed to update user');
} finally {
setIsUpdating(false);
}
},
[userId, router]
);
const avatarInputRef = React.useRef<HTMLInputElement>(null);
const avatar = watch('avatar');
const handleAvatarChange = React.useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const url = await fileToBase64(file);
setValue('avatar', url);
}
},
[setValue]
);
// TODO: need to align with save form
// use trycatch
const [textDescription, setTextDescription] = React.useState<string>('');
const [textRemarks, setTextRemarks] = React.useState<string>('');
// load existing data when user arrive
const loadExistingData = React.useCallback(
async (id: string) => {
setShowLoading(true);
try {
const result = await pb.collection(COL_USERS).getOne(id);
reset({ ...defaultValues, ...result });
console.log({ result });
if (result.avatar_file) {
const fetchResult = await fetch(
`http://127.0.0.1:8090/api/files/${result.collectionId}/${result.id}/${result.avatar_file}`
);
const blob = await fetchResult.blob();
const url = await fileToBase64(blob);
setValue('avatar', url);
}
} catch (error) {
logger.error(error);
toast.error('Failed to load user data');
setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
} finally {
setShowLoading(false);
}
},
[reset, setValue]
);
React.useEffect(() => {
void loadExistingData(userId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId]);
if (showLoading) return <FormLoading />;
if (showError.show)
return (
<ErrorDisplay
message={t('error.unable-to-process-request')}
code="500"
details={showError.detail}
/>
);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Card>
<CardContent>
<Stack
divider={<Divider />}
spacing={4}
>
<Stack spacing={3}>
<Typography variant="h6">{t('edit.basic-info')}</Typography>
<Grid
container
spacing={3}
>
<Grid xs={12}>
<Stack
direction="row"
spacing={3}
sx={{ alignItems: 'center' }}
>
<Box
sx={{
border: '1px dashed var(--mui-palette-divider)',
borderRadius: '5%',
display: 'inline-flex',
p: '4px',
}}
>
<Avatar
variant="rounded"
src={avatar}
sx={{
'--Avatar-size': '100px',
'--Icon-fontSize': 'var(--icon-fontSize-lg)',
alignItems: 'center',
bgcolor: 'var(--mui-palette-background-level1)',
color: 'var(--mui-palette-text-primary)',
display: 'flex',
justifyContent: 'center',
}}
>
<CameraIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
</Box>
<Stack
spacing={1}
sx={{ alignItems: 'flex-start' }}
>
<Typography variant="subtitle1">{t('edit.avatar')}</Typography>
<Typography variant="caption">{t('edit.avatarRequirements')}</Typography>
<Button
color="secondary"
onClick={() => {
avatarInputRef.current?.click();
}}
variant="outlined"
>
{t('edit.avatar_select')}
</Button>
<input
hidden
onChange={handleAvatarChange}
ref={avatarInputRef}
type="file"
/>
</Stack>
</Stack>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="name"
render={({ field }) => (
<FormControl
error={Boolean(errors.name)}
fullWidth
>
<InputLabel required>Name</InputLabel>
<OutlinedInput {...field} />
{errors.name ? <FormHelperText>{errors.name.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="email"
render={({ field }) => (
<FormControl
error={Boolean(errors.email)}
fullWidth
>
<InputLabel required>Email</InputLabel>
<OutlinedInput
{...field}
type="email"
/>
{errors.email ? <FormHelperText>{errors.email.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="phone"
render={({ field }) => (
<FormControl
error={Boolean(errors.phone)}
fullWidth
>
<InputLabel required>Phone</InputLabel>
<OutlinedInput {...field} />
{errors.phone ? <FormHelperText>{errors.phone.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="company"
render={({ field }) => (
<FormControl
error={Boolean(errors.company)}
fullWidth
>
<InputLabel>Company</InputLabel>
<OutlinedInput
{...field}
placeholder="no company name"
/>
{errors.company ? <FormHelperText>{errors.company.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
{/* */}
<Stack spacing={3}>
<Typography variant="h6">Billing Information</Typography>
<Grid
container
spacing={3}
>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="billingAddress.country"
render={({ field }) => (
<FormControl
error={Boolean(errors.billingAddress?.country)}
fullWidth
>
<InputLabel required>Country</InputLabel>
<Select {...field}>
<MenuItem value="US">United States</MenuItem>
<MenuItem value="UK">United Kingdom</MenuItem>
<MenuItem value="CA">Canada</MenuItem>
</Select>
{errors.billingAddress?.country ? (
<FormHelperText>{errors.billingAddress.country.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="billingAddress.state"
render={({ field }) => (
<FormControl
error={Boolean(errors.billingAddress?.state)}
fullWidth
>
<InputLabel required>State</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.state ? (
<FormHelperText>{errors.billingAddress.state.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="billingAddress.city"
render={({ field }) => (
<FormControl
error={Boolean(errors.billingAddress?.city)}
fullWidth
>
<InputLabel required>City</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.city ? (
<FormHelperText>{errors.billingAddress.city.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="billingAddress.zipCode"
render={({ field }) => (
<FormControl
error={Boolean(errors.billingAddress?.zipCode)}
fullWidth
>
<InputLabel required>Zip Code</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.zipCode ? (
<FormHelperText>{errors.billingAddress.zipCode.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="billingAddress.line1"
render={({ field }) => (
<FormControl
error={Boolean(errors.billingAddress?.line1)}
fullWidth
>
<InputLabel required>Address Line 1</InputLabel>
<OutlinedInput {...field} />
{errors.billingAddress?.line1 ? (
<FormHelperText>{errors.billingAddress.line1.message}</FormHelperText>
) : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="taxId"
render={({ field }) => (
<FormControl
error={Boolean(errors.taxId)}
fullWidth
>
<InputLabel>Tax ID</InputLabel>
<OutlinedInput
{...field}
placeholder="no tax id..."
/>
{errors.taxId ? <FormHelperText>{errors.taxId.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
<Stack spacing={3}>
<Typography variant="h6">Additional Information</Typography>
<Grid
container
spacing={3}
>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="timezone"
render={({ field }) => (
<FormControl
error={Boolean(errors.timezone)}
fullWidth
>
<InputLabel required>Timezone</InputLabel>
<Select {...field}>
<MenuItem value="America/New_York">New York</MenuItem>
<MenuItem value="Europe/London">London</MenuItem>
<MenuItem value="Asia/Tokyo">Tokyo</MenuItem>
<MenuItem value="America/Boa_Vista">Boa Vista</MenuItem>
<MenuItem value="America/Grand_Turk">Grand Turk</MenuItem>
<MenuItem value="Asia/Manila">Manila</MenuItem>
<MenuItem value="Asia/Urumqi">Urumqi</MenuItem>
<MenuItem value="Africa/Tunis">Tunis</MenuItem>
</Select>
{errors.timezone ? <FormHelperText>{errors.timezone.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="language"
render={({ field }) => (
<FormControl
error={Boolean(errors.language)}
fullWidth
>
<InputLabel required>Language</InputLabel>
<Select {...field}>
<MenuItem value="en">English</MenuItem>
<MenuItem value="es">Spanish</MenuItem>
<MenuItem value="fr">French</MenuItem>
</Select>
{errors.language ? <FormHelperText>{errors.language.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
<Grid
md={6}
xs={12}
>
<Controller
control={control}
name="currency"
render={({ field }) => (
<FormControl
error={Boolean(errors.currency)}
fullWidth
>
<InputLabel required>Currency</InputLabel>
<Select {...field}>
<MenuItem value="USD">USD</MenuItem>
<MenuItem value="EUR">EUR</MenuItem>
<MenuItem value="GBP">GBP</MenuItem>
</Select>
{errors.currency ? <FormHelperText>{errors.currency.message}</FormHelperText> : null}
</FormControl>
)}
/>
</Grid>
</Grid>
</Stack>
</Stack>
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}>
<Button
color="secondary"
component={RouterLink}
href={paths.dashboard.users.list}
>
{t('edit.cancelButton')}
</Button>
<LoadingButton
disabled={isUpdating}
loading={isUpdating}
type="submit"
variant="contained"
>
{t('edit.updateButton')}
</LoadingButton>
</CardActions>
</Card>
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
<pre>{JSON.stringify({ errors }, null, 2)}</pre>
</Box>
</form>
);
}

View File

@@ -0,0 +1,242 @@
'use client';
// RULES:
// T.B.A.
//
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { getAllCustomersCount } from '@/db/Customers/GetAllCount';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
import Select from '@mui/material/Select';
import type { SelectChangeEvent } from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { paths } from '@/paths';
import { FilterButton } from '@/components/core/filter-button';
import { Option } from '@/components/core/option';
import { useCustomersSelection } from './users-selection-context';
import GetBlockedCount from '@/db/Customers/GetBlockedCount';
import GetPendingCount from '@/db/Customers/GetPendingCount';
import GetActiveCount from '@/db/Customers/GetActiveCount';
import PhoneFilterPopover from './phone-filter-popover';
import EmailFilterPopover from './email-filter-popover';
import type { CustomersFiltersProps, Filters, SortDir } from './type.d';
export function UsersFilters({ filters = {}, sortDir = 'desc', fullData }: CustomersFiltersProps): React.JSX.Element {
const { t } = useTranslation();
const { email, phone, status } = filters;
const [totalCount, setTotalCount] = React.useState<number>(0);
const [activeCount, setActiveCount] = React.useState<number>(0);
const [pendingCount, setPendingCount] = React.useState<number>(0);
const [blockedCount, setBlockedCount] = React.useState<number>(0);
const router = useRouter();
const selection = useCustomersSelection();
// function getVisible(): number {
// return fullData.reduce((count, item: CrQuestion) => {
// return item.visible === 'visible' ? count + 1 : count;
// }, 0);
// }
// function getHidden(): number {
// return fullData.reduce((count, item: CrQuestion) => {
// return item.visible === 'hidden' ? count + 1 : count;
// }, 0);
// }
// The tabs should be generated using API data.
const tabs = [
{ label: 'All', value: '', count: totalCount },
{ label: 'Active', value: 'active', count: activeCount },
{ label: 'Pending', value: 'pending', count: pendingCount },
{ label: 'Blocked', value: 'blocked', count: blockedCount },
] as const;
const updateSearchParams = React.useCallback(
(newFilters: Filters, newSortDir: SortDir): void => {
const searchParams = new URLSearchParams();
if (newSortDir === 'asc') {
searchParams.set('sortDir', newSortDir);
}
if (newFilters.status) {
searchParams.set('status', newFilters.status);
}
if (newFilters.email) {
searchParams.set('email', newFilters.email);
}
if (newFilters.phone) {
searchParams.set('phone', newFilters.phone);
}
router.push(`${paths.dashboard.customers.list}?${searchParams.toString()}`);
},
[router]
);
const handleClearFilters = React.useCallback(() => {
updateSearchParams({}, sortDir);
}, [updateSearchParams, sortDir]);
const handleStatusChange = React.useCallback(
(_: React.SyntheticEvent, value: string) => {
updateSearchParams({ ...filters, status: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleEmailChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, email: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handlePhoneChange = React.useCallback(
(value?: string) => {
updateSearchParams({ ...filters, phone: value }, sortDir);
},
[updateSearchParams, filters, sortDir]
);
const handleSortChange = React.useCallback(
(event: SelectChangeEvent) => {
updateSearchParams(filters, event.target.value as SortDir);
},
[updateSearchParams, filters]
);
const hasFilters = status || email || phone;
React.useEffect(() => {
const fetchCount = async (): Promise<void> => {
try {
const tc = await getAllCustomersCount();
setTotalCount(tc);
const bc = await GetBlockedCount();
setBlockedCount(bc);
const pc = await GetPendingCount();
setPendingCount(pc);
const ac = await GetActiveCount();
setActiveCount(ac);
} catch (error) {
//
}
};
void fetchCount();
}, []);
return (
<div>
<Tabs
onChange={handleStatusChange}
sx={{ px: 3 }}
value={status ?? ''}
variant="scrollable"
//
>
{tabs.map((tab) => (
<Tab
icon={
<Chip
label={tab.count}
size="small"
variant="soft"
/>
}
iconPosition="end"
key={tab.value}
label={tab.label}
sx={{ minHeight: 'auto' }}
tabIndex={0}
value={tab.value}
/>
))}
</Tabs>
<Divider />
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flexWrap: 'wrap', px: 3, py: 2 }}
>
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flex: '1 1 auto', flexWrap: 'wrap' }}
>
<FilterButton
displayValue={email}
label="Email"
onFilterApply={(value) => {
handleEmailChange(value as string);
}}
onFilterDelete={() => {
handleEmailChange();
}}
popover={<EmailFilterPopover />}
value={email}
/>
<FilterButton
displayValue={phone}
label="Phone number"
onFilterApply={(value) => {
handlePhoneChange(value as string);
}}
onFilterDelete={() => {
handlePhoneChange();
}}
popover={<PhoneFilterPopover />}
value={phone}
/>
{hasFilters ? <Button onClick={handleClearFilters}>Clear filters</Button> : null}
</Stack>
{selection.selectedAny ? (
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center' }}
>
<Typography
color="text.secondary"
variant="body2"
>
{selection.selected.size} selected
</Typography>
<Button
color="error"
variant="contained"
>
Delete
</Button>
</Stack>
) : null}
<Select
name="sort"
onChange={handleSortChange}
sx={{ maxWidth: '100%', width: '120px' }}
value={sortDir}
>
<Option value="desc">Newest</Option>
<Option value="asc">Oldest</Option>
</Select>
</Stack>
</div>
);
}

View File

@@ -0,0 +1,50 @@
'use client';
import * as React from 'react';
import TablePagination from '@mui/material/TablePagination';
function noop(): void {
return undefined;
}
interface UsersPaginationProps {
count: number;
page: number;
//
setPage: (page: number) => void;
setRowsPerPage: (page: number) => void;
rowsPerPage: number;
}
export function UsersPagination({
count,
page,
//
setPage,
setRowsPerPage,
rowsPerPage,
}: UsersPaginationProps): React.JSX.Element {
// You should implement the pagination using a similar logic as the filters.
// Note that when page change, you should keep the filter search params.
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value));
// console.log(parseInt(event.target.value));
};
return (
<TablePagination
component="div"
count={count}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
page={page}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[5, 10, 25]}
//
/>
);
}

View File

@@ -0,0 +1,40 @@
'use client';
import * as React from 'react';
import { useSelection } from '@/hooks/use-selection';
import type { Selection } from '@/hooks/use-selection';
import type { User } from './type.d';
function noop(): void {
return undefined;
}
export interface CustomersSelectionContextValue extends Selection {}
export const CustomersSelectionContext = React.createContext<CustomersSelectionContextValue>({
deselectAll: noop,
deselectOne: noop,
selectAll: noop,
selectOne: noop,
selected: new Set(),
selectedAny: false,
selectedAll: false,
});
interface UsersSelectionProviderProps {
children: React.ReactNode;
users: User[];
}
export function UsersSelectionProvider({ children, users = [] }: UsersSelectionProviderProps): React.JSX.Element {
const customerIds = React.useMemo(() => users.map((customer) => customer.id), [users]);
const selection = useSelection(customerIds);
return <CustomersSelectionContext.Provider value={{ ...selection }}>{children}</CustomersSelectionContext.Provider>;
}
export function useCustomersSelection(): CustomersSelectionContextValue {
return React.useContext(CustomersSelectionContext);
}

View File

@@ -0,0 +1,222 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { LoadingButton } from '@mui/lab';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import IconButton from '@mui/material/IconButton';
import LinearProgress from '@mui/material/LinearProgress';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock';
import { Images as ImagesIcon } from '@phosphor-icons/react/dist/ssr/Images';
import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus';
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
import { TrashSimple as TrashSimpleIcon } from '@phosphor-icons/react/dist/ssr/TrashSimple';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { paths } from '@/paths';
import { dayjs } from '@/lib/dayjs';
import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table';
import ConfirmDeleteModal from './confirm-delete-modal';
import { useCustomersSelection } from './users-selection-context';
import type { User } from './type.d';
function columns(handleDeleteClick: (testId: string) => void): ColumnDef<User>[] {
return [
{
formatter: (row): React.JSX.Element => (
<Stack
direction="row"
spacing={1}
sx={{ alignItems: 'center' }}
>
<Avatar src={row.avatar} />{' '}
<div>
<Link
color="inherit"
component={RouterLink}
href={paths.dashboard.users.view(row.id)}
sx={{ whiteSpace: 'nowrap' }}
variant="subtitle2"
>
{row.name}
</Link>
<Typography
color="text.secondary"
variant="body2"
>
{row.email}
</Typography>
</div>
</Stack>
),
name: 'Name',
width: '250px',
},
{
formatter: (row): React.JSX.Element => (
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center' }}
>
<LinearProgress
sx={{ flex: '1 1 auto' }}
value={row.quota}
variant="determinate"
/>
<Typography
color="text.secondary"
variant="body2"
>
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)}
</Typography>
</Stack>
),
name: 'Quota',
width: '150px',
},
{ field: 'phone', name: 'Phone number', width: '150px' },
{
formatter: (row): React.JSX.Element => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const mapping = {
active: {
label: 'Active',
icon: (
<CheckCircleIcon
color="var(--mui-palette-success-main)"
weight="fill"
/>
),
},
blocked: { label: 'Blocked', icon: <MinusIcon color="var(--mui-palette-error-main)" /> },
pending: {
label: 'Pending',
icon: (
<ClockIcon
color="var(--mui-palette-warning-main)"
weight="fill"
/>
),
},
} as const;
const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null };
return (
<Chip
icon={icon}
label={label}
size="small"
variant="outlined"
/>
);
},
name: 'Status',
width: '150px',
},
{
formatter(row) {
return dayjs(row.createdAt).format('MMM D, YYYY');
},
name: 'Created at',
width: '150px',
},
{
formatter: (row): React.JSX.Element => (
<Stack
direction="row"
spacing={1}
>
<LoadingButton
//
color="secondary"
component={RouterLink}
href={paths.dashboard.users.view(row.id)}
>
<PencilSimpleIcon size={24} />
</LoadingButton>
<LoadingButton
color="error"
// TODO: originally it is row.isEmpty
disabled={false}
onClick={() => {
handleDeleteClick(row.id);
}}
>
<TrashSimpleIcon size={24} />
</LoadingButton>
</Stack>
),
name: 'Actions',
hideName: true,
align: 'right',
},
];
}
export interface UsersTableProps {
rows: User[];
reloadRows: () => void;
}
export function UsersTable({ rows, reloadRows }: UsersTableProps): React.JSX.Element {
const { t } = useTranslation(['customers']);
const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCustomersSelection();
const [idToDelete, setIdToDelete] = React.useState('');
const [open, setOpen] = React.useState(false);
function handleDeleteClick(testId: string): void {
setOpen(true);
setIdToDelete(testId);
}
return (
<React.Fragment>
<ConfirmDeleteModal
idToDelete={idToDelete}
open={open}
reloadRows={reloadRows}
setOpen={setOpen}
/>
<DataTable<User>
columns={columns(handleDeleteClick)}
onDeselectAll={deselectAll}
onDeselectOne={(_, row) => {
deselectOne(row.id);
}}
onSelectAll={selectAll}
onSelectOne={(_, row) => {
selectOne(row.id);
}}
rows={rows}
selectable
selected={selected}
/>
{!rows.length ? (
<Box sx={{ p: 3 }}>
<Typography
color="text.secondary"
sx={{ textAlign: 'center' }}
variant="body2"
>
{/* TODO: update this */}
{t('no-record-found')}
</Typography>
</Box>
) : null}
</React.Fragment>
);
}

View File

@@ -1,6 +1,6 @@
// RULES:
// COL_<COLLECTION_NAME> = "<table name in dbml file>"
// e.g. COL_APPLE = "Apple" table in dbml
// e.g. COL_APPLE = collection "Apple" in pocketbase = "Apple" table in dbml
const COL_LESSON_TYPES = 'LessonsTypes';
const COL_LESSON_CATEGORIES = 'LessonsCategories';
const COL_USERS = 'users';

View File

@@ -1,9 +1,9 @@
import { COL_CUSTOMERS } from '@/constants';
import { COL_CUSTOMERS, COL_USER_METAS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetActiveCount(): Promise<number> {
const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, {
filter: 'status = "active"',
const { totalItems: count } = await pb.collection(COL_USER_METAS).getList(1, 1, {
filter: 'role = "teacher" && status = "active"',
});
return count;
}

View File

@@ -1,9 +1,9 @@
import { COL_CUSTOMERS } from '@/constants';
import { COL_CUSTOMERS, COL_USER_METAS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetBlockedCount(): Promise<number> {
const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, {
filter: 'status = "blocked"',
const { totalItems: count } = await pb.collection(COL_USER_METAS).getList(1, 1, {
filter: `role = "teacher" && status = "blocked"`,
});
return count;
}

View File

@@ -1,9 +1,9 @@
import { COL_CUSTOMERS } from '@/constants';
import { COL_CUSTOMERS, COL_USER_METAS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetPendingCount(): Promise<number> {
const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, {
filter: 'status = "pending"',
const { totalItems: count } = await pb.collection(COL_USER_METAS).getList(1, 1, {
filter: 'role = "teacher" && status = "pending"',
});
return count;
}

View File

@@ -1,6 +1,6 @@
# GUIDELINES
This folder contains drivers for `Customer`/`Customers` records using PocketBase:
This folder contains drivers for `Customer`/`Customers`(Collection ID: pbc_108570809) records using PocketBase:
- create (Create.tsx)
- read (GetById.tsx)

View File

@@ -1,6 +1,6 @@
# GUIDELINES
This folder contains test drivers for `Helloworld` records using PocketBase:
This folder contains test drivers for `Helloworld`(Collection ID: pbc_123408445) records using PocketBase:
- create (Create.tsx)
- read (GetById.tsx)

View File

@@ -1,6 +1,6 @@
# GUIDELINES
This folder contains drivers for `LessonCategory`/`LessonCategories` records using PocketBase:
This folder contains drivers for `LessonCategory`/`LessonCategories`(Collection ID: pbc_1196309394) records using PocketBase:
- create (Create.tsx)
- read (GetById.tsx)

View File

@@ -1,6 +1,6 @@
# GUIDELINES
This folder contains drivers for `LessonType`/`LessonTypes` records using PocketBase:
This folder contains drivers for `LessonType`/`LessonTypes`(Collection ID: pbc_2328411368) records using PocketBase:
- create (Create.tsx)
- read (GetById.tsx)

View File

@@ -1,6 +1,8 @@
# GUIDELINES
This folder contains drivers for `Customer`/`Customers` records using PocketBase:
This folder contains drivers for `Notification`/`Notifications` (Collection ID: pbc_977978967) records using PocketBase:
## File Structure
- create (Create.tsx)
- read (GetById.tsx)
@@ -8,24 +10,66 @@ This folder contains drivers for `Customer`/`Customers` records using PocketBase
- count (GetAllCount.tsx, GetActiveCount.tsx, GetBlockedCount.tsx, GetPendingCount.tsx)
- misc (Helloworld.tsx)
- delete (Delete.tsx)
- list (GetAll.tsx)
- list (GetAll.tsx, GetNotificationByUserId.tsx)
the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src`
## Assumption and Requirements
- assume `pb` is located in `@/lib/pb`
- no need to handle error in this function, i'll handle it in the caller
- type information defined in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers/type.d.tsx`
simple template:
## Implementation Template
```typescript
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
import { COL_NOTIFICATIONS } from '@/constants';
import type { CreateNotificationProps } from './type.d.ts';
export async function createCustomer(data: CreateFormProps) {
// ...content
// use direct return of pb.collection (e.g. return pb.collection(xxx))
export async function createNotification(data: CreateNotificationProps) {
return pb.collection(COL_NOTIFICATIONS).create(data);
}
```
## Special Considerations
- User-specific notifications (GetNotificationByUserId.tsx)
- Status transitions (active/pending/blocked)
- Priority levels handling
- Expiration dates
- Bulk operations support
## Type Definitions
Key types to use:
- `NotificationStatus`: active|pending|blocked
- `NotificationPriority`: low|medium|high
- `CreateNotificationProps`: Required fields
- `UpdateNotificationProps`: Partial updates
## Common Patterns
```typescript
// Bulk creation example
export async function createBulkNotifications(items: CreateNotificationProps[]) {
return Promise.all(items.map(item =>
pb.collection(COL_NOTIFICATIONS).create(item)
));
}
// Status update example
export async function markAsRead(id: string) {
return pb.collection(COL_NOTIFICATIONS).update(id, { status: 'read' });
}
```
## Performance Notes
- Ensure indexes on: user_id, status, created_at
- Consider pagination for large notification lists
- Cache frequently accessed notifications
- Batch operations for mass notifications
## Testing Guidelines
Recommended test cases:
- Single notification creation
- Bulk operations
- Status transitions
- User-specific queries
- Error cases (invalid data, permissions)

View File

@@ -1,6 +1,6 @@
# GUIDELINES
This folder contains drivers for `QuizCRCategory`/`QuizCRCategories` records using PocketBase:
This folder contains drivers for `QuizCRCategory`/`QuizCRCategories`(Collection ID: pbc_4061499106) records using PocketBase:
- create (Create.tsx)
- read (GetById.tsx)

View File

@@ -1,6 +1,6 @@
# GUIDELINES
This folder contains drivers for `QuizLPCategory` records using PocketBase:
This folder contains drivers for `QuizLPCategory`(Collection ID: pbc_3639453778) records using PocketBase:
- create (Create.tsx)
- read (GetById.tsx)

View File

@@ -1,6 +1,8 @@
# GUIDELINES
This folder contains drivers for `QuizLPQuestion` records using PocketBase:
This folder contains drivers for `QuizLPQuestion`/`QuizLPQuestions` records using PocketBase:
## File Structure
- create (Create.tsx)
- read (GetById.tsx)
@@ -8,27 +10,72 @@ This folder contains drivers for `QuizLPQuestion` records using PocketBase:
- count (GetAllCount.tsx)
- delete (Delete.tsx)
- list (GetAll.tsx)
- validation (validateQuestion.tsx)
the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src`
## Assumption and Requirements
- assume `pb` is located in `@/lib/pb`
- no need to handle error in this function, i'll handle it in the caller
- type information defined in `@/db/QuizLPQuestions/type.d.tsx`
- Quiz LP questions require special handling for:
- Answer validation
- Question type checking
- Category association
simple template:
## Complete Implementation Template
```typescript
import { pb } from '@/lib/pb';
import { COL_QUIZ_LP_QUESTIONS } from '@/constants';
import type { QuizLPQuestion, CreateFormProps } from './type.d.ts';
export async function createQuizLPQuestion(data: CreateFormProps) {
// ...content
// use direct return of pb.collection (e.g. return pb.collection(COL_QUIZ_LP_QUESTIONS))
export async function createQuizLPQuestion(data: CreateFormProps): Promise<QuizLPQuestion> {
return pb.collection(COL_QUIZ_LP_QUESTIONS).create(data);
}
```
## Question-Specific Handling
### Answer Validation
```typescript
export function validateLPQuestionAnswer(question: QuizLPQuestion, answer: string): boolean {
// Implement LP question specific validation
return question.correctAnswer === answer;
}
```
### Type Definitions
```typescript
interface QuizLPQuestion {
id: string;
question: string;
options: string[];
correctAnswer: string;
categoryId: string;
difficulty: 'easy'|'medium'|'hard';
}
```
## Common Patterns
```typescript
// Get questions by category
export async function getQuestionsByCategory(categoryId: string) {
return pb.collection(COL_QUIZ_LP_QUESTIONS)
.getFullList({ filter: `categoryId = "${categoryId}"` });
}
// Validate before create
export async function createValidatedQuestion(data: CreateFormProps) {
if (!validateQuestionData(data)) throw new Error('Invalid question data');
return createQuizLPQuestion(data);
}
```
## Performance Considerations
- Add index on: categoryId, difficulty
- Consider pagination for large question sets
- Cache frequently accessed questions
## Testing Guidelines
Recommended test cases:
- Basic CRUD operations
- Answer validation
- Category filtering
- Difficulty level queries
- Error cases (invalid data)

View File

@@ -0,0 +1,11 @@
// Student type definitions
export interface Student {
id: string;
name: string;
avatar: string;
email: string;
phone: string;
quota: number;
status: 'active' | 'blocked' | 'pending';
createdAt: Date;
}

View File

@@ -1,6 +1,6 @@
import { pb } from '@/lib/pb';
import { COL_TEACHERS } from '@/constants';
import { COL_USER_METAS } from '@/constants';
export async function deleteTeacher(id: string): Promise<boolean> {
return pb.collection(COL_TEACHERS).delete(id);
return pb.collection(COL_USER_METAS).delete(id);
}

View File

@@ -1,8 +1,8 @@
import { COL_TEACHERS } from '@/constants';
import { COL_TEACHERS, COL_USER_METAS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetActiveCount(): Promise<number> {
const { totalItems: count } = await pb.collection(COL_TEACHERS).getList(1, 1, {
const { totalItems: count } = await pb.collection(COL_USER_METAS).getList(1, 1, {
filter: 'status = "active"',
});
return count;

View File

@@ -1,7 +1,7 @@
import { pb } from '@/lib/pb';
import { COL_TEACHERS } from '@/constants';
import { COL_USER_METAS } from '@/constants';
export async function getAllTeachersCount(): Promise<number> {
const result = await pb.collection(COL_TEACHERS).getList(1, 1);
const result = await pb.collection(COL_USER_METAS).getList(1, 1, { filter: `role = "teacher"` });
return result.totalItems;
}

View File

@@ -0,0 +1,11 @@
// api method for crate customer record
// RULES:
// TBA
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
import type { CreateFormProps } from '@/components/dashboard/customer/type.d';
import type { RecordModel } from 'pocketbase';
export async function createCustomer(data: CreateFormProps): Promise<RecordModel> {
return pb.collection(COL_CUSTOMERS).create(data);
}

View File

@@ -0,0 +1,6 @@
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
export async function deleteCustomer(id: string): Promise<boolean> {
return pb.collection(COL_CUSTOMERS).delete(id);
}

View File

@@ -0,0 +1,9 @@
import { COL_CUSTOMERS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetActiveCount(): Promise<number> {
const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, {
filter: 'status = "active"',
});
return count;
}

View File

@@ -0,0 +1,7 @@
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
import { RecordModel } from 'pocketbase';
export async function getAllCustomers(options = {}): Promise<RecordModel[]> {
return pb.collection(COL_CUSTOMERS).getFullList(options);
}

View File

@@ -0,0 +1,9 @@
import { COL_CUSTOMERS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetBlockedCount(): Promise<number> {
const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, {
filter: 'status = "blocked"',
});
return count;
}

View File

@@ -0,0 +1,7 @@
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
import { RecordModel } from 'pocketbase';
export async function getCustomerById(id: string): Promise<RecordModel> {
return pb.collection(COL_CUSTOMERS).getOne(id);
}

View File

@@ -0,0 +1,9 @@
import { COL_CUSTOMERS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetPendingCount(): Promise<number> {
const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, {
filter: 'status = "pending"',
});
return count;
}

View File

@@ -0,0 +1,3 @@
export function helloCustomer() {
return 'Hello from Customers module!';
}

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