Compare commits
32 Commits
develop/cm
...
b8e8968866
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b8e8968866 | ||
![]() |
92040c6efb | ||
![]() |
a3d2ee57f7 | ||
![]() |
90835a7fe3 | ||
![]() |
ca2a9c235b | ||
![]() |
29b074f6dd | ||
![]() |
2f8acbbcdf | ||
![]() |
41a35b487a | ||
![]() |
7a33549a79 | ||
![]() |
d81b3e9a9e | ||
![]() |
da08798b10 | ||
![]() |
41cc82d54d | ||
![]() |
c8d184465a | ||
![]() |
adc04a1a40 | ||
![]() |
62da367589 | ||
![]() |
04ac1a8881 | ||
![]() |
dfd6ecc744 | ||
![]() |
69cb0718be | ||
![]() |
7dc7716f18 | ||
![]() |
633a1d3a4c | ||
![]() |
64b5f89fdf | ||
![]() |
f87dd2c3b1 | ||
![]() |
8b3dfe69e4 | ||
![]() |
375b0a3593 | ||
![]() |
bd907b4dde | ||
![]() |
73a2b2dfb9 | ||
![]() |
2f28d71060 | ||
![]() |
01ce517629 | ||
![]() |
033ca95dfe | ||
![]() |
0fa277c50e | ||
![]() |
e8ded0cb30 | ||
![]() |
cc6e40c9d0 |
@@ -0,0 +1,45 @@
|
|||||||
|
# AI GUIDELINE
|
||||||
|
|
||||||
|
## getting started
|
||||||
|
|
||||||
|
Imagine there is a software developer and a QA engineer to solve the problems together
|
||||||
|
|
||||||
|
They will:
|
||||||
|
|
||||||
|
no need to reply me what you are going on and your digest in this phase.
|
||||||
|
just reply me "OK" when done
|
||||||
|
|
||||||
|
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
|
||||||
|
|
||||||
|
- `schema.dbml`
|
||||||
|
|
||||||
|
- read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
|
||||||
|
this is file in dbml syntax state the main database
|
||||||
|
|
||||||
|
- `schema.json`
|
||||||
|
|
||||||
|
- read `<base_dir>/002_source/cms/src/db/schema.json`
|
||||||
|
this is the file of live pocketbase schema output
|
||||||
|
|
||||||
|
- read `<base_dir>/002_source/cms/src/constants.ts`
|
||||||
|
this is the content of `@/constants`
|
||||||
|
|
||||||
|
- look into the md files in folder `<base_dir>/002_source/cms/_AI_WORKSPACE/001_guideline`
|
||||||
|
|
||||||
|
- read, remember and link up the ideas in file stated above,
|
||||||
|
i will tell them the task afterwards
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Hi, i have 2 files
|
||||||
|
`schema.json` is a export from pocketbase (e.g. collections and fields)
|
||||||
|
`schema.dbml` is schema file for presentation of the database (e.g. tables)
|
||||||
|
|
||||||
|
please take a look in `schema.dbml` and `schema.json`,
|
||||||
|
associate the existing `collection` from json file to the `table` in dbml file
|
||||||
|
|
||||||
|
keep `schema.json` remain unchanged and update `schema.dbml` to match it.
|
||||||
|
|
||||||
|
please modify the `schema.dbml` to align with `schema.json`
|
||||||
|
|
||||||
|
to the collection `QuizLPCategories` align the dbml file in the previous prompt
|
@@ -1,215 +1,266 @@
|
|||||||
|
// Users table with auth fields
|
||||||
|
Table Users {
|
||||||
|
// system field
|
||||||
|
id text [pk]
|
||||||
|
tokenKey text [not null]
|
||||||
|
created datetime [default: `now()`]
|
||||||
|
updated datetime
|
||||||
|
password text [not null]
|
||||||
|
|
||||||
|
// value field
|
||||||
|
email text [not null]
|
||||||
|
emailVisibility boolean
|
||||||
|
verified boolean
|
||||||
|
name text
|
||||||
|
avatar file
|
||||||
|
}
|
||||||
|
|
||||||
// LessonTypes stores different types of lessons
|
// LessonTypes stores different types of lessons
|
||||||
// lesson_types, lesson_type
|
// lesson_types, lesson_type
|
||||||
Table LessonTypes {
|
Table LessonTypes {
|
||||||
// system field
|
// system field
|
||||||
id int [pk, increment] // unique identifier for the lesson type
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`] // timestamp when the lesson type was created
|
created datetime [default: `now()`] // timestamp when the lesson type was created
|
||||||
updated datetime // timestamp when the lesson type was last updated
|
updated datetime // timestamp when the lesson type was last updated
|
||||||
// value field
|
// value field
|
||||||
name varchar // name of the lesson type
|
name text // changed from varchar to text
|
||||||
type varchar // type category
|
type text // changed from varchar to text
|
||||||
|
pos integer
|
||||||
|
visible text
|
||||||
|
field date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// LessonCategories stores categories of lessons
|
// LessonCategories stores categories of lessons
|
||||||
// lesson_categories, lesson_category
|
// lesson_categories, lesson_category
|
||||||
Table LessonCategories {
|
Table LessonCategories {
|
||||||
// system field
|
// system field
|
||||||
id int [pk, increment] // unique identifier for the lesson category
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`] // timestamp when the category was created
|
created datetime [default: `now()`] // timestamp when the category was created
|
||||||
updated datetime // timestamp when the category was last updated
|
updated datetime // timestamp when the category was last updated
|
||||||
// value field
|
// value field
|
||||||
cat_name varchar // image file name
|
cat_name text // changed from varchar to text
|
||||||
cat_image varchar // image representing the category
|
cat_image_url text // new field
|
||||||
lesson_type_id integer [ref: > LessonTypes.id] // foreign key referencing LessonTypes.id
|
cat_image file // changed from varchar to file
|
||||||
|
pos integer
|
||||||
|
lesson_id integer [ref: > LessonTypes.id] // foreign key referencing LessonTypes.id
|
||||||
|
description text // new field
|
||||||
|
remarks text // changed from varchar to text
|
||||||
|
visible text // new field
|
||||||
}
|
}
|
||||||
|
|
||||||
Table Helloworlds {
|
Table Helloworlds {
|
||||||
// system field
|
// system field
|
||||||
id int [pk, increment] // id field, increment
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`] // record create time
|
created datetime [default: `now()`] // record create time
|
||||||
updated datetime // record update time
|
updated datetime // record update time
|
||||||
}
|
hello text // new field
|
||||||
|
|
||||||
Table Users {
|
|
||||||
// system field
|
|
||||||
id int [pk, increment]
|
|
||||||
created datetime [default: `now()`]
|
|
||||||
updated datetime
|
|
||||||
|
|
||||||
// value field
|
|
||||||
email varchar
|
|
||||||
emailVisibility boolean
|
|
||||||
verified boolean
|
|
||||||
name varchar
|
|
||||||
avatar blob
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Table UserMetas {
|
Table UserMetas {
|
||||||
// system field
|
// system field
|
||||||
id int [pk, increment]
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`]
|
created datetime [default: `now()`]
|
||||||
updated datetime
|
updated datetime
|
||||||
|
|
||||||
// value field
|
// value field
|
||||||
helloworld varchar
|
helloworld text // changed from varchar to text
|
||||||
app_on_time_s integer
|
meta json // new field
|
||||||
user_id integer [ref: > Users.id]
|
user_id text [ref: > Users.id] // changed type and reference
|
||||||
|
state text // new field
|
||||||
|
avatar file // changed from blob to file
|
||||||
|
role text // new field
|
||||||
}
|
}
|
||||||
|
|
||||||
Table QuizCategories {
|
Table QuizCategories {
|
||||||
// system field
|
// system field
|
||||||
id int [pk, increment]
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`]
|
created datetime [default: `now()`]
|
||||||
updated datetime
|
updated datetime
|
||||||
|
|
||||||
// value field
|
// value field
|
||||||
cat_name varchar // category name
|
cat_name text // changed from varchar to text
|
||||||
cat_image varchar // category image
|
cat_image text // changed from varchar to text
|
||||||
|
init_answer json // new field
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// stores all questions of matching frenzy
|
// stores all questions of matching frenzy
|
||||||
Table QuizMatchings {
|
Table QuizMatchings {
|
||||||
// system field
|
// system field
|
||||||
id int [pk, increment] // id field, increment
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`] // record create time
|
created datetime [default: `now()`] // record create time
|
||||||
updated datetime // record update time
|
updated datetime // record update time
|
||||||
|
|
||||||
// value field
|
// value field
|
||||||
word varchar // modal answer
|
word text // changed from varchar to text
|
||||||
word_c varchar // question
|
word_c text // changed from varchar to text
|
||||||
cat_id integer [ref: > QuizCategories.id] // foreign key to QuizCategories.id
|
cat_id text [ref: > QuizCategories.id] // changed type and reference
|
||||||
}
|
}
|
||||||
|
|
||||||
// QuizListening stores all listening quiz data
|
// QuizListening stores all listening quiz data
|
||||||
Table QuizListenings {
|
Table QuizListenings {
|
||||||
// system field
|
// system field
|
||||||
id int [pk, increment] // id field, increment
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`] // record create time
|
created datetime [default: `now()`] // record create time
|
||||||
updated datetime // record update time
|
updated datetime // record update time
|
||||||
|
|
||||||
// value field
|
// value field
|
||||||
sound varchar // URL to the sound file
|
sound file // changed from varchar to file
|
||||||
word varchar // The word in the quiz
|
word text // changed from varchar to text
|
||||||
cat_id integer [ref: > QuizCategories.id]
|
cat_id text [ref: > QuizCategories.id] // changed type and reference
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// stores all categories of connectives revision quiz
|
// stores all categories of connectives revision quiz
|
||||||
Table QuizConnectivesCategories {
|
Table QuizConnectivesCategories {
|
||||||
// system field
|
// system field
|
||||||
id int [pk, increment] // id field, increment
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`] // record create time
|
created datetime [default: `now()`] // record create time
|
||||||
updated datetime // record update time
|
updated datetime // record update time
|
||||||
|
|
||||||
// value field
|
// value field
|
||||||
cat_name varchar // category name
|
cat_name text // changed from varchar to text
|
||||||
cat_image varchar // category image
|
cat_image file // changed from varchar to file
|
||||||
}
|
}
|
||||||
|
|
||||||
// stores all questions of connectives revision quiz
|
// stores all questions of connectives revision quiz
|
||||||
Table QuizConnectives {
|
Table QuizConnectives {
|
||||||
// system field
|
// system field
|
||||||
id int [pk, increment] // id field, increment
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`] // record create time
|
created datetime [default: `now()`] // record create time
|
||||||
updated datetime // record update time
|
updated datetime // record update time
|
||||||
|
|
||||||
// value field
|
// value field
|
||||||
question_fh varchar // first half
|
question_fh text // changed from varchar to text
|
||||||
question_sh varchar // second half
|
question_sh text // changed from varchar to text
|
||||||
modal_ans varchar // modal ans
|
modal_ans text // changed from varchar to text
|
||||||
cat_id integer [ref: > QuizConnectivesCategories.id] // foreign key to QuizConnectivesCategories.id
|
cat_id text [ref: > QuizConnectivesCategories.id] // changed type and reference
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lessons stores all lessons in the database
|
// Lessons stores all lessons in the database
|
||||||
Table Vocabularies {
|
Table Vocabularies {
|
||||||
// system field
|
// system field
|
||||||
id int [pk, increment] // unique identifier for the lesson
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`] // timestamp when the lesson was created
|
created datetime [default: `now()`] // timestamp when the lesson was created
|
||||||
updated datetime // timestamp when the lesson was last updated
|
updated datetime // timestamp when the lesson was last updated
|
||||||
|
|
||||||
// value field
|
// value field
|
||||||
image varchar // URL to the image associated with the lesson
|
image file // changed from varchar to file
|
||||||
sound varchar // URL to the sound file associated with the lesson
|
sound file // changed from varchar to file
|
||||||
word varchar // The word in English
|
word text // changed from varchar to text
|
||||||
word_c varchar // The word in Chinese
|
word_c text // changed from varchar to text
|
||||||
sample_e varchar // Sample sentence in English using the word
|
sample_e text // changed from varchar to text
|
||||||
sample_c varchar // Sample sentence in Chinese using the word
|
sample_c text // changed from varchar to text
|
||||||
cat_id integer [ref: > LessonCategories.id] // foreign key referring to LessonCategories.id
|
cat_id text [ref: > LessonCategories.id] // changed type and reference
|
||||||
category varchar // The category to which the lesson belongs
|
category text // changed from varchar to text
|
||||||
lesson_type_id integer [ref: > LessonTypes.id] // foreign key referring to LessonTypes.id
|
lesson_type_id text [ref: > LessonTypes.id] // changed type and reference
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listening Practice Quiz Categories
|
// Listening Practice Quiz Categories
|
||||||
// store listening practice category, (LpCategories, LpCategory)
|
// store listening practice category, (LpCategories, LpCategory)
|
||||||
Table QuizLPCategories {
|
Table QuizLPCategories {
|
||||||
// system fields
|
// system fields
|
||||||
id text [pk] // changed from int to text to match PocketBase
|
id text [pk]
|
||||||
created datetime [default: `now()`]
|
created datetime [default: `now()`]
|
||||||
updated datetime
|
updated datetime
|
||||||
|
|
||||||
// value fields
|
// value fields
|
||||||
cat_name varchar [presentable: true] // added presentable flag
|
cat_name text
|
||||||
cat_image file // changed from blob to file type
|
cat_image file
|
||||||
pos number // changed from integer to number
|
pos number
|
||||||
init_answer json
|
init_answer json
|
||||||
|
visible text
|
||||||
|
slug text
|
||||||
|
remarks text
|
||||||
|
description text
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listening Practice Quiz Questions
|
// Listening Practice Quiz Questions
|
||||||
Table QuizLPQuestions {
|
Table QuizLPQuestions {
|
||||||
id int [pk, increment]
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`]
|
created datetime [default: `now()`]
|
||||||
updated datetime
|
updated datetime
|
||||||
word varchar
|
word text // changed from varchar to text
|
||||||
sound blob
|
sound file // changed from blob to file
|
||||||
cat_id integer [ref: > QuizLPCategories.id]
|
cat_id text [ref: > QuizLPCategories.id] // changed type and reference
|
||||||
|
cat_name text // new field
|
||||||
|
cat_image file // new field
|
||||||
|
pos number // new field
|
||||||
|
init_answer json // new field
|
||||||
|
visible text // new field
|
||||||
|
slug text // new field
|
||||||
|
remarks text // new field
|
||||||
|
description text // new field
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matching Frenzy Quiz Categories
|
// Matching Frenzy Quiz Categories
|
||||||
Table QuizMFCategories {
|
Table QuizMFCategories {
|
||||||
id int [pk, increment]
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`]
|
created datetime [default: `now()`]
|
||||||
updated datetime
|
updated datetime
|
||||||
cat_name varchar
|
cat_name text // changed from varchar to text
|
||||||
cat_image blob
|
cat_image file // changed from blob to file
|
||||||
pos integer
|
pos number // changed from integer to number
|
||||||
init_answer json
|
init_answer json
|
||||||
|
visible text // new field
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matching Frenzy Quiz Questions
|
// Matching Frenzy Quiz Questions
|
||||||
Table QuizMFQuestions {
|
Table QuizMFQuestions {
|
||||||
id int [pk, increment]
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`]
|
created datetime [default: `now()`]
|
||||||
updated datetime
|
updated datetime
|
||||||
word varchar
|
word text // changed from varchar to text
|
||||||
word_c varchar
|
word_c text // changed from varchar to text
|
||||||
cat_id integer [ref: > QuizMFCategories.id]
|
cat_id text [ref: > QuizMFCategories.id] // changed type and reference
|
||||||
|
visible text // new field
|
||||||
|
sound file // new field
|
||||||
|
cat_image file // new field
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connectives Revision Quiz Categories
|
// Connectives Revision Quiz Categories
|
||||||
Table QuizCRCategories {
|
Table QuizCRCategories {
|
||||||
id int [pk, increment]
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`]
|
created datetime [default: `now()`]
|
||||||
updated datetime
|
updated datetime
|
||||||
cat_name varchar
|
cat_name text // changed from varchar to text
|
||||||
cat_image blob
|
cat_image file // changed from blob to file
|
||||||
pos integer
|
pos integer
|
||||||
init_answer json
|
init_answer json
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connectives Revision Quiz Questions
|
// Connectives Revision Quiz Questions
|
||||||
Table QuizCRQuestions {
|
Table QuizCRQuestions {
|
||||||
id int [pk, increment]
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`]
|
created datetime [default: `now()`]
|
||||||
updated datetime
|
updated datetime
|
||||||
question_fh varchar
|
question_fh text // changed from varchar to text
|
||||||
question_sh varchar
|
question_sh text // changed from varchar to text
|
||||||
modal_ans varchar
|
modal_ans text // changed from varchar to text
|
||||||
cat_id integer [ref: > QuizCRCategories.id]
|
cat_id text [ref: > QuizCRCategories.id] // changed type and reference
|
||||||
|
options json // new field
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test table
|
// Test table
|
||||||
Table t1 {
|
Table t1 {
|
||||||
id int [pk, increment]
|
id text [pk] // changed from int to text
|
||||||
created datetime [default: `now()`]
|
created datetime [default: `now()`]
|
||||||
updated datetime
|
updated datetime
|
||||||
name varchar
|
hello text // changed from name to hello
|
||||||
|
test_file file // new field
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customers table
|
||||||
|
Table Customers {
|
||||||
|
id text [pk] // new table
|
||||||
|
created datetime [default: `now()`]
|
||||||
|
updated datetime
|
||||||
|
name text
|
||||||
|
email text
|
||||||
|
phone text
|
||||||
|
quota number
|
||||||
|
status text
|
||||||
|
avatar_file file
|
||||||
|
cat_id text [ref: > QuizMFCategories.id] // refer to a single user in `Users` table
|
||||||
}
|
}
|
||||||
|
215
001_documentation/Requirements/REQ0006/schema.dbml.ai_draft
Normal file
215
001_documentation/Requirements/REQ0006/schema.dbml.ai_draft
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
// LessonTypes stores different types of lessons
|
||||||
|
// lesson_types, lesson_type
|
||||||
|
Table LessonTypes {
|
||||||
|
// system field
|
||||||
|
id int [pk, increment] // unique identifier for the lesson type
|
||||||
|
created datetime [default: `now()`] // timestamp when the lesson type was created
|
||||||
|
updated datetime // timestamp when the lesson type was last updated
|
||||||
|
// value field
|
||||||
|
name varchar // name of the lesson type
|
||||||
|
type varchar // type category
|
||||||
|
}
|
||||||
|
|
||||||
|
// LessonCategories stores categories of lessons
|
||||||
|
// lesson_categories, lesson_category
|
||||||
|
Table LessonCategories {
|
||||||
|
// system field
|
||||||
|
id int [pk, increment] // unique identifier for the lesson category
|
||||||
|
created datetime [default: `now()`] // timestamp when the category was created
|
||||||
|
updated datetime // timestamp when the category was last updated
|
||||||
|
// value field
|
||||||
|
cat_name varchar // image file name
|
||||||
|
cat_image varchar // image representing the category
|
||||||
|
lesson_type_id integer [ref: > LessonTypes.id] // foreign key referencing LessonTypes.id
|
||||||
|
}
|
||||||
|
|
||||||
|
Table Helloworlds {
|
||||||
|
// system field
|
||||||
|
id int [pk, increment] // id field, increment
|
||||||
|
created datetime [default: `now()`] // record create time
|
||||||
|
updated datetime // record update time
|
||||||
|
}
|
||||||
|
|
||||||
|
Table Users {
|
||||||
|
// system field
|
||||||
|
id int [pk, increment]
|
||||||
|
created datetime [default: `now()`]
|
||||||
|
updated datetime
|
||||||
|
|
||||||
|
// value field
|
||||||
|
email varchar
|
||||||
|
emailVisibility boolean
|
||||||
|
verified boolean
|
||||||
|
name varchar
|
||||||
|
avatar blob
|
||||||
|
}
|
||||||
|
|
||||||
|
Table UserMetas {
|
||||||
|
// system field
|
||||||
|
id int [pk, increment]
|
||||||
|
created datetime [default: `now()`]
|
||||||
|
updated datetime
|
||||||
|
|
||||||
|
// value field
|
||||||
|
helloworld varchar
|
||||||
|
app_on_time_s integer
|
||||||
|
user_id integer [ref: > Users.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
Table QuizCategories {
|
||||||
|
// system field
|
||||||
|
id int [pk, increment]
|
||||||
|
created datetime [default: `now()`]
|
||||||
|
updated datetime
|
||||||
|
|
||||||
|
// value field
|
||||||
|
cat_name varchar // category name
|
||||||
|
cat_image varchar // category image
|
||||||
|
}
|
||||||
|
|
||||||
|
// stores all questions of matching frenzy
|
||||||
|
Table QuizMatchings {
|
||||||
|
// system field
|
||||||
|
id int [pk, increment] // id field, increment
|
||||||
|
created datetime [default: `now()`] // record create time
|
||||||
|
updated datetime // record update time
|
||||||
|
|
||||||
|
// value field
|
||||||
|
word varchar // modal answer
|
||||||
|
word_c varchar // question
|
||||||
|
cat_id integer [ref: > QuizCategories.id] // foreign key to QuizCategories.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuizListening stores all listening quiz data
|
||||||
|
Table QuizListenings {
|
||||||
|
// system field
|
||||||
|
id int [pk, increment] // id field, increment
|
||||||
|
created datetime [default: `now()`] // record create time
|
||||||
|
updated datetime // record update time
|
||||||
|
|
||||||
|
// value field
|
||||||
|
sound varchar // URL to the sound file
|
||||||
|
word varchar // The word in the quiz
|
||||||
|
cat_id integer [ref: > QuizCategories.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
// stores all categories of connectives revision quiz
|
||||||
|
Table QuizConnectivesCategories {
|
||||||
|
// system field
|
||||||
|
id int [pk, increment] // id field, increment
|
||||||
|
created datetime [default: `now()`] // record create time
|
||||||
|
updated datetime // record update time
|
||||||
|
|
||||||
|
// value field
|
||||||
|
cat_name varchar // category name
|
||||||
|
cat_image varchar // category image
|
||||||
|
}
|
||||||
|
|
||||||
|
// stores all questions of connectives revision quiz
|
||||||
|
Table QuizConnectives {
|
||||||
|
// system field
|
||||||
|
id int [pk, increment] // id field, increment
|
||||||
|
created datetime [default: `now()`] // record create time
|
||||||
|
updated datetime // record update time
|
||||||
|
|
||||||
|
// value field
|
||||||
|
question_fh varchar // first half
|
||||||
|
question_sh varchar // second half
|
||||||
|
modal_ans varchar // modal ans
|
||||||
|
cat_id integer [ref: > QuizConnectivesCategories.id] // foreign key to QuizConnectivesCategories.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lessons stores all lessons in the database
|
||||||
|
Table Vocabularies {
|
||||||
|
// system field
|
||||||
|
id int [pk, increment] // unique identifier for the lesson
|
||||||
|
created datetime [default: `now()`] // timestamp when the lesson was created
|
||||||
|
updated datetime // timestamp when the lesson was last updated
|
||||||
|
|
||||||
|
// value field
|
||||||
|
image varchar // URL to the image associated with the lesson
|
||||||
|
sound varchar // URL to the sound file associated with the lesson
|
||||||
|
word varchar // The word in English
|
||||||
|
word_c varchar // The word in Chinese
|
||||||
|
sample_e varchar // Sample sentence in English using the word
|
||||||
|
sample_c varchar // Sample sentence in Chinese using the word
|
||||||
|
cat_id integer [ref: > LessonCategories.id] // foreign key referring to LessonCategories.id
|
||||||
|
category varchar // The category to which the lesson belongs
|
||||||
|
lesson_type_id integer [ref: > LessonTypes.id] // foreign key referring to LessonTypes.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listening Practice Quiz Categories
|
||||||
|
// store listening practice category, (LpCategories, LpCategory)
|
||||||
|
Table QuizLPCategories {
|
||||||
|
// system fields
|
||||||
|
id text [pk] // changed from int to text to match PocketBase
|
||||||
|
created datetime [default: `now()`]
|
||||||
|
updated datetime
|
||||||
|
|
||||||
|
// value fields
|
||||||
|
cat_name varchar [presentable: true] // added presentable flag
|
||||||
|
cat_image file // changed from blob to file type
|
||||||
|
pos number // changed from integer to number
|
||||||
|
init_answer json
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listening Practice Quiz Questions
|
||||||
|
Table QuizLPQuestions {
|
||||||
|
id int [pk, increment]
|
||||||
|
created datetime [default: `now()`]
|
||||||
|
updated datetime
|
||||||
|
word varchar
|
||||||
|
sound blob
|
||||||
|
cat_id integer [ref: > QuizLPCategories.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matching Frenzy Quiz Categories
|
||||||
|
Table QuizMFCategories {
|
||||||
|
id int [pk, increment]
|
||||||
|
created datetime [default: `now()`]
|
||||||
|
updated datetime
|
||||||
|
cat_name varchar
|
||||||
|
cat_image blob
|
||||||
|
pos integer
|
||||||
|
init_answer json
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matching Frenzy Quiz Questions
|
||||||
|
Table QuizMFQuestions {
|
||||||
|
id int [pk, increment]
|
||||||
|
created datetime [default: `now()`]
|
||||||
|
updated datetime
|
||||||
|
word varchar
|
||||||
|
word_c varchar
|
||||||
|
cat_id integer [ref: > QuizMFCategories.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connectives Revision Quiz Categories
|
||||||
|
Table QuizCRCategories {
|
||||||
|
id int [pk, increment]
|
||||||
|
created datetime [default: `now()`]
|
||||||
|
updated datetime
|
||||||
|
cat_name varchar
|
||||||
|
cat_image blob
|
||||||
|
pos integer
|
||||||
|
init_answer json
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connectives Revision Quiz Questions
|
||||||
|
Table QuizCRQuestions {
|
||||||
|
id int [pk, increment]
|
||||||
|
created datetime [default: `now()`]
|
||||||
|
updated datetime
|
||||||
|
question_fh varchar
|
||||||
|
question_sh varchar
|
||||||
|
modal_ans varchar
|
||||||
|
cat_id integer [ref: > QuizCRCategories.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test table
|
||||||
|
Table t1 {
|
||||||
|
id int [pk, increment]
|
||||||
|
created datetime [default: `now()`]
|
||||||
|
updated datetime
|
||||||
|
name varchar
|
||||||
|
}
|
@@ -1,7 +1,6 @@
|
|||||||
const { resolve } = require('node:path');
|
const { resolve } = require('node:path');
|
||||||
|
|
||||||
const project = resolve(__dirname, 'tsconfig.json');
|
const project = resolve(__dirname, 'tsconfig.json');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
extends: [
|
extends: [
|
||||||
@@ -84,4 +83,13 @@ module.exports = {
|
|||||||
'react/jsx-sort-props': 'off',
|
'react/jsx-sort-props': 'off',
|
||||||
},
|
},
|
||||||
ignorePatterns: ['**/*del', '**/*bak', '**/*copy.*', '**/*copy*.*'],
|
ignorePatterns: ['**/*del', '**/*bak', '**/*copy.*', '**/*copy*.*'],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
// override to ignore no-def for `describe`, `it`, and `expect`
|
||||||
|
files: ['*.test.ts', '*.test.tsx'],
|
||||||
|
rules: {
|
||||||
|
'no-undef': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
# guideline
|
|
||||||
|
|
||||||
- please divide the problem into small parts
|
|
||||||
- if you found youself cannot understand the problem, please stop and ask how to do
|
|
||||||
- if you found youself cannot solve the problem, plesae stop and ask how to do
|
|
||||||
- review the whole solution before you reply to user
|
|
||||||
- if code syntax is already there, do follow (e.g. naming convention, syntax) the existing code
|
|
||||||
|
|
||||||
- example for page can be found in `./src/app/_helloworld/page.tsx`
|
|
||||||
- example for component can be found in `./src/components/_helloworld/index.tsx`
|
|
||||||
|
|
||||||
- no need to explain the reason until you are told to do so
|
|
||||||
- no need to show me the code change, at the end just simple summary in point form is ok
|
|
||||||
|
|
||||||
Thanks
|
|
@@ -1,10 +0,0 @@
|
|||||||
Hi, i need your help.
|
|
||||||
|
|
||||||
i am working on a react typescript project
|
|
||||||
i will show you part of the code and it is located in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/mf`
|
|
||||||
|
|
||||||
it is copied from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lp`, i want you to help modifing from `lp` to `mf`, the corrosponding word is `lp -> listening practice` and `mf -> matching frenzy`,
|
|
||||||
|
|
||||||
please help to update the `imports`, variables/constants, functions, classes
|
|
||||||
|
|
||||||
thank you
|
|
44
002_source/cms/_AI_WORKSPACE/code/001_INIT.md
Normal file
44
002_source/cms/_AI_WORKSPACE/code/001_INIT.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# 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
|
52
002_source/cms/_AI_WORKSPACE/code/002_nextjs.mdc
Normal file
52
002_source/cms/_AI_WORKSPACE/code/002_nextjs.mdc
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
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
|
57
002_source/cms/_AI_WORKSPACE/code/003_typescript.mdc
Normal file
57
002_source/cms/_AI_WORKSPACE/code/003_typescript.mdc
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
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
|
44
002_source/cms/_AI_WORKSPACE/db/001_INIT.md
Normal file
44
002_source/cms/_AI_WORKSPACE/db/001_INIT.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# 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
|
22
002_source/cms/_AI_WORKSPACE/db/002_PROMPT.md
Normal file
22
002_source/cms/_AI_WORKSPACE/db/002_PROMPT.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Hi, i need your help.
|
||||||
|
|
||||||
|
i am working on a nextjs react typescript project
|
||||||
|
|
||||||
|
please read the code in directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPQuestions` (denoted `QuizLPQuestions`)
|
||||||
|
|
||||||
|
i want you to:
|
||||||
|
|
||||||
|
- list files from directory `QuizLPQuestions` and use them as template
|
||||||
|
- understand the relations (e.g. `lp` -> `lp_categories`, `lp` -> `COL_QUIZ_LP_CATEGORIES`)
|
||||||
|
- review if any remaining `lp` or `mf` exist in `cr` directory and please help to do replace it to `cr`. (e.g. `COL_QUIZ_LP_CATEGORIES` -> `COL_QUIZ_CR_CATEGORIES` )
|
||||||
|
- please create if you find any missing files/codes/constants
|
||||||
|
- using template, create the similar code for `cr` named `QuizCRCategories`
|
||||||
|
|
||||||
|
thank you
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- I proofed the code is working already, what you need to do is refactoring of the `variables`, `functions`, `classes`, `constants` (`lp` -> `cr`)
|
||||||
|
- compare the difference between `lp` and `mf`,
|
||||||
|
- remember the differences and
|
||||||
|
- draft the new type `cr` (e.g. modify the `import` locations, variables, functions, classes, constants name etc.)
|
22
002_source/cms/_AI_WORKSPACE/db/002_PROMPT_categories.md
Normal file
22
002_source/cms/_AI_WORKSPACE/db/002_PROMPT_categories.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Hi, i need your help.
|
||||||
|
|
||||||
|
i am working on a nextjs react typescript project
|
||||||
|
|
||||||
|
please read the code in directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPCategories` (denoted `QuizLPCategories`)
|
||||||
|
|
||||||
|
i want you to:
|
||||||
|
|
||||||
|
- list files from directory `QuizLPCategories` and use them as template
|
||||||
|
- understand the relations (e.g. `lp` -> `lp_categories`, `lp` -> `COL_QUIZ_LP_CATEGORIES`)
|
||||||
|
- review if any remaining `lp` or `mf` exist in `cr` directory and please help to do replace it to `cr`. (e.g. `COL_QUIZ_LP_CATEGORIES` -> `COL_QUIZ_CR_CATEGORIES` )
|
||||||
|
- please create if you find any missing files/codes/constants
|
||||||
|
- using template, create the similar code for `cr` named `QuizCRCategories`
|
||||||
|
|
||||||
|
thank you
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- I proofed the code is working already, what you need to do is refactoring of the `variables`, `functions`, `classes`, `constants` (`lp` -> `cr`)
|
||||||
|
- compare the difference between `lp` and `mf`,
|
||||||
|
- remember the differences and
|
||||||
|
- draft the new type `cr` (e.g. modify the `import` locations, variables, functions, classes, constants name etc.)
|
22
002_source/cms/_AI_WORKSPACE/db/002_PROMPT_questions.md
Normal file
22
002_source/cms/_AI_WORKSPACE/db/002_PROMPT_questions.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Hi, i need your help.
|
||||||
|
|
||||||
|
i am working on a nextjs react typescript project
|
||||||
|
|
||||||
|
please read the code in directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizLPQuestions` (denoted `QuizLPQuestions`)
|
||||||
|
|
||||||
|
i want you to:
|
||||||
|
|
||||||
|
- list files from directory `QuizLPQuestions` and use them as template
|
||||||
|
- understand the relations (e.g. `lp` -> `lp_Questions`, `lp` -> `COL_QUIZ_LP_QUESTIONS`)
|
||||||
|
- review if any remaining `lp` or `mf` exist in `cr` directory and please help to do replace it to `cr`. (e.g. `COL_QUIZ_LP_QUESTIONS` -> `COL_QUIZ_CR_Questions` )
|
||||||
|
- please create if you find any missing files/codes/constants
|
||||||
|
- using template, create the similar code for `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.)
|
@@ -0,0 +1,20 @@
|
|||||||
|
Hi, i need your help.
|
||||||
|
|
||||||
|
## task
|
||||||
|
|
||||||
|
i am working on a dbml file
|
||||||
|
i got a schema.json from exported from pocketbase
|
||||||
|
and i want to update it to my current dbml file (for documentation)
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- the collection from json file started with `_` can be ignored. they are system collection and should not appear in dbml
|
||||||
|
- one collection from json file mapped with one table in dbml file
|
||||||
|
- the `presentable` field from json file should be ignored.
|
||||||
|
|
||||||
|
## information
|
||||||
|
|
||||||
|
json file: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/schema.json`
|
||||||
|
dbml file: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml`
|
||||||
|
|
||||||
|
thanks
|
10
002_source/cms/_AI_WORKSPACE/db/004_clone_db_driver.md
Normal file
10
002_source/cms/_AI_WORKSPACE/db/004_clone_db_driver.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
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/Teachers`
|
||||||
|
|
||||||
|
change all occurrence of `customer` to `teacher` thanks.
|
18
002_source/cms/_AI_WORKSPACE/db/009_draft_db_drivers.md
Normal file
18
002_source/cms/_AI_WORKSPACE/db/009_draft_db_drivers.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 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.
|
@@ -0,0 +1,10 @@
|
|||||||
|
# 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.
|
BIN
002_source/cms/_AI_WORKSPACE/db/Code_XE50rE8nMK.mp4
Normal file
BIN
002_source/cms/_AI_WORKSPACE/db/Code_XE50rE8nMK.mp4
Normal file
Binary file not shown.
@@ -1,36 +1,3 @@
|
|||||||
# AI GUIDELINE
|
|
||||||
|
|
||||||
## getting started
|
|
||||||
|
|
||||||
Imagine there is a software developer and a QA engineer to solve the problems together
|
|
||||||
|
|
||||||
They will:
|
|
||||||
|
|
||||||
no need to reply me what you are going on and your digest in this phase.
|
|
||||||
just reply me "OK" when done
|
|
||||||
|
|
||||||
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
|
|
||||||
|
|
||||||
- `schema.dbml`
|
|
||||||
|
|
||||||
- read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
|
|
||||||
this is file in dbml syntax state the main database
|
|
||||||
|
|
||||||
- `schema.json`
|
|
||||||
|
|
||||||
- read `<base_dir>/002_source/cms/src/db/schema.json`
|
|
||||||
this is the file of live pocketbase schema output
|
|
||||||
|
|
||||||
- read `<base_dir>/002_source/cms/src/constants.ts`
|
|
||||||
this is the content of `@/constants`
|
|
||||||
|
|
||||||
- look into the md files in folder `<base_dir>/002_source/cms/_AI_WORKSPACE/001_guideline`
|
|
||||||
|
|
||||||
- read, remember and link up the ideas in file stated above,
|
|
||||||
i will tell them the task afterwards
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
The software engineer will provide solutions,
|
The software engineer will provide solutions,
|
||||||
while QA engineer will feedback the opinion.
|
while QA engineer will feedback the opinion.
|
||||||
|
|
38
002_source/cms/_AI_WORKSPACE/get_summary/001_INIT.md
Normal file
38
002_source/cms/_AI_WORKSPACE/get_summary/001_INIT.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# AI GUIDELINE
|
||||||
|
|
||||||
|
## getting started
|
||||||
|
|
||||||
|
Imagine there is a
|
||||||
|
|
||||||
|
1. software developer and a
|
||||||
|
2. QA engineer
|
||||||
|
3. technical writer
|
||||||
|
|
||||||
|
to solve the problems together
|
||||||
|
|
||||||
|
They will:
|
||||||
|
|
||||||
|
no need to reply me what you are going on and your digest in this phase.
|
||||||
|
just reply me "OK" when done
|
||||||
|
|
||||||
|
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
|
||||||
|
|
||||||
|
- `schema.dbml`
|
||||||
|
|
||||||
|
- read `<base_dir>/001_documentation/Requirements/REQ0006/schema.dbml`
|
||||||
|
this is file in `dbml` format stating the main database structure
|
||||||
|
|
||||||
|
- `schema.json`
|
||||||
|
|
||||||
|
- read `<base_dir>/002_source/cms/src/db/schema.json`
|
||||||
|
this is the file of current pocketbase schema
|
||||||
|
|
||||||
|
- read `<base_dir>/002_source/cms/src/constants.ts`
|
||||||
|
this is the content of `@/constants`
|
||||||
|
|
||||||
|
- look into the md files in folder `<base_dir>/002_source/cms/_AI_WORKSPACE/001_guideline`
|
||||||
|
|
||||||
|
- directory may contain `repomix-output.xml` file, that is a simple summary of all files inside the directory
|
||||||
|
|
||||||
|
- read, remember and link up the ideas in file stated above,
|
||||||
|
i will tell them the task afterwards
|
15
002_source/cms/_AI_WORKSPACE/get_summary/002_PROMPT.md
Normal file
15
002_source/cms/_AI_WORKSPACE/get_summary/002_PROMPT.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
Hi, i need your help.
|
||||||
|
|
||||||
|
## background
|
||||||
|
|
||||||
|
i am working on a nextjs react typescript project
|
||||||
|
|
||||||
|
## task
|
||||||
|
|
||||||
|
i want you to summarize the code, write it to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories/_PROMPT.md`
|
||||||
|
|
||||||
|
## steps
|
||||||
|
|
||||||
|
1. list all `tsx` files from directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories`
|
||||||
|
2. take a look to the `tsx` file listed. try to understand what's going on (e.g. it is a component for `cr-categories`, it is a table)
|
||||||
|
3. write your result to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories/_PROMPT.md`
|
@@ -0,0 +1,17 @@
|
|||||||
|
Hi, i need your help.
|
||||||
|
|
||||||
|
## background
|
||||||
|
|
||||||
|
i am working on a nextjs react typescript project
|
||||||
|
|
||||||
|
## task
|
||||||
|
|
||||||
|
i want you to summarize the code, write it to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/questions/_SUMMARY.md`
|
||||||
|
|
||||||
|
## steps
|
||||||
|
|
||||||
|
1. list all `tsx` files from directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/questions`
|
||||||
|
2. read every `tsx` files listed. try to understand what's going on (e.g. it is a component for `cr-questions`, it is a table)
|
||||||
|
3. read file `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/questions/_SUMMARY.md`.
|
||||||
|
4. make use of `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/questions/_SUMMARY.md` as template, update the content and create a new summary
|
||||||
|
5. write your new summary to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/questions/_SUMMARY.md`
|
@@ -0,0 +1,17 @@
|
|||||||
|
Hi, i need your help.
|
||||||
|
|
||||||
|
## background
|
||||||
|
|
||||||
|
i am working on a nextjs react typescript project
|
||||||
|
|
||||||
|
## task
|
||||||
|
|
||||||
|
i want you to summarize the code, write it to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/categories/_SUMMARY.md`
|
||||||
|
|
||||||
|
## steps
|
||||||
|
|
||||||
|
1. list all `tsx` files from directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/categories`
|
||||||
|
2. read every `tsx` files listed. try to understand what's going on (e.g. it is a component for `cr-categories`, it is a table)
|
||||||
|
3. read file `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories/_SUMMARY.md`.
|
||||||
|
4. make use of `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/cr/categories/_SUMMARY.md` as template, update the content and create a new summary
|
||||||
|
5. write your new summary to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp/categories/_SUMMARY.md`
|
66
002_source/cms/_AI_WORKSPACE/get_summary/DB_PROMPT.MD
Normal file
66
002_source/cms/_AI_WORKSPACE/get_summary/DB_PROMPT.MD
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
The software engineer will provide solutions,
|
||||||
|
while QA engineer will feedback the opinion.
|
||||||
|
|
||||||
|
this is now not in debug phase,
|
||||||
|
so, no need to reply me what they are going on or their insight throught the prompt.
|
||||||
|
just reply me "OK" when done
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it
|
||||||
|
|
||||||
|
please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden`
|
||||||
|
|
||||||
|
well done !, please proceed to another request
|
||||||
|
|
||||||
|
working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db`
|
||||||
|
|
||||||
|
according information from `schema.json`, get the collection of `Students`
|
||||||
|
|
||||||
|
pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content
|
||||||
|
|
||||||
|
when you draft coding, review file and append with `.tsx.draft`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- this is part of react typescript project, with pocketbase
|
||||||
|
- `schema.dbml`, describe the collections(tables)
|
||||||
|
- folder `LessonCategories`, the correct references
|
||||||
|
- folder `LessonTypes`, the correct references
|
||||||
|
- you can find the `schema.dbml` and schema information from `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006`
|
||||||
|
- do not read root directory, assume it is a fresh copy of nextjs project is ok
|
||||||
|
|
||||||
|
## instruction
|
||||||
|
|
||||||
|
- break the questions into smaller parts
|
||||||
|
- review file append with `.draft`, see if the content aligned with the correct references
|
||||||
|
- read and understand `dbml` file
|
||||||
|
- lookup the every folder
|
||||||
|
|
||||||
|
## tasks
|
||||||
|
|
||||||
|
Thanks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
please revise
|
||||||
|
|
||||||
|
please revise
|
||||||
|
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx` `interface LpCategory`
|
||||||
|
|
||||||
|
to the collection `QuizLPCategories` align the dbml file in the previous prompt
|
||||||
|
|
||||||
|
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lp_categories/_constants.tsx`
|
||||||
|
|
||||||
|
to follow the type definition in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/types/LpCategory.tsx`, the constant `defaultLpCategory`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
the constants file (`@/constants`) was `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/constants.ts`
|
||||||
|
|
||||||
|
please help to fix the `tsx` files in folder `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/QuizMFCategories`,
|
||||||
|
the `COL` constants is wrongly used, it should refer to `COL_QUIZ_MF_CATEGORIES`. thanks
|
||||||
|
|
||||||
|
please update the `COL_XXXX` TO COL_MF_CATEGORIES
|
1
002_source/cms/_AI_WORKSPACE/init.md
Normal file
1
002_source/cms/_AI_WORKSPACE/init.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
please read, understand and remember `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/_AI_WORKSPACE/code`
|
@@ -22,8 +22,8 @@
|
|||||||
"test": "jest --watch",
|
"test": "jest --watch",
|
||||||
"update_doc": "cd 001_documentation/Requirements && node update_req_index.js",
|
"update_doc": "cd 001_documentation/Requirements && node update_req_index.js",
|
||||||
"update_doc:w": "pnpx nodemon --ext md --exec \"pnpm run update_doc\"",
|
"update_doc:w": "pnpx nodemon --ext md --exec \"pnpm run update_doc\"",
|
||||||
"update_repomix": "npx repomix",
|
"update_repomix": "node ./scripts/update_repomix.js",
|
||||||
"update_repomix:w": "pnpx nodemon --delay 3 --exec \"pnpx repomix\"",
|
"update_repomix:w": "pnpx nodemon --delay 3 --exec \"npm run update_repomix\"",
|
||||||
"clean": "rm -rf node_modules && rm -rf .next"
|
"clean": "rm -rf node_modules && rm -rf .next"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -91,6 +91,7 @@
|
|||||||
"zod": "3.22.4"
|
"zod": "3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "^9.7.0",
|
||||||
"@ianvs/prettier-plugin-sort-imports": "4.1.1",
|
"@ianvs/prettier-plugin-sort-imports": "4.1.1",
|
||||||
"@testing-library/jest-dom": "6.4.2",
|
"@testing-library/jest-dom": "6.4.2",
|
||||||
"@testing-library/react": "14.2.1",
|
"@testing-library/react": "14.2.1",
|
||||||
|
9
002_source/cms/pnpm-lock.yaml
generated
9
002_source/cms/pnpm-lock.yaml
generated
@@ -195,6 +195,9 @@ importers:
|
|||||||
specifier: 3.22.4
|
specifier: 3.22.4
|
||||||
version: 3.22.4
|
version: 3.22.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@faker-js/faker':
|
||||||
|
specifier: ^9.7.0
|
||||||
|
version: 9.7.0
|
||||||
'@ianvs/prettier-plugin-sort-imports':
|
'@ianvs/prettier-plugin-sort-imports':
|
||||||
specifier: 4.1.1
|
specifier: 4.1.1
|
||||||
version: 4.1.1(prettier@3.2.5)
|
version: 4.1.1(prettier@3.2.5)
|
||||||
@@ -725,6 +728,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==}
|
resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
|
||||||
|
'@faker-js/faker@9.7.0':
|
||||||
|
resolution: {integrity: sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg==}
|
||||||
|
engines: {node: '>=18.0.0', npm: '>=9.0.0'}
|
||||||
|
|
||||||
'@fastify/busboy@2.1.0':
|
'@fastify/busboy@2.1.0':
|
||||||
resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==}
|
resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -6303,6 +6310,8 @@ snapshots:
|
|||||||
|
|
||||||
'@eslint/js@8.57.0': {}
|
'@eslint/js@8.57.0': {}
|
||||||
|
|
||||||
|
'@faker-js/faker@9.7.0': {}
|
||||||
|
|
||||||
'@fastify/busboy@2.1.0': {}
|
'@fastify/busboy@2.1.0': {}
|
||||||
|
|
||||||
'@firebase/analytics-compat@0.2.7(@firebase/app-compat@0.2.27)(@firebase/app@0.9.27)':
|
'@firebase/analytics-compat@0.2.7(@firebase/app-compat@0.2.27)(@firebase/app@0.9.27)':
|
||||||
|
@@ -28,7 +28,9 @@ const config = {
|
|||||||
'',
|
'',
|
||||||
'^[./]',
|
'^[./]',
|
||||||
],
|
],
|
||||||
plugins: ['@ianvs/prettier-plugin-sort-imports'],
|
plugins: [
|
||||||
|
// '@ianvs/prettier-plugin-sort-imports'
|
||||||
|
],
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['*.tsx'],
|
files: ['*.tsx'],
|
||||||
|
7
002_source/cms/scripts/005_update_repomix.sh
Executable file
7
002_source/cms/scripts/005_update_repomix.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
node ./scripts/update_repomix.js
|
||||||
|
|
||||||
|
echo "done"
|
7
002_source/cms/scripts/006_test.sh
Executable file
7
002_source/cms/scripts/006_test.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
node ./scripts/update_repomix.js
|
||||||
|
|
||||||
|
echo "done"
|
8
002_source/cms/scripts/999_express_commit.sh
Executable file
8
002_source/cms/scripts/999_express_commit.sh
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
git add src
|
||||||
|
git commit -m'update,'
|
||||||
|
|
||||||
|
echo "done"
|
@@ -1,19 +1,23 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
set -ex
|
set -x
|
||||||
|
|
||||||
# ls **/.next
|
# ls **/.next
|
||||||
# ls **/.pnpm
|
# ls **/.pnpm
|
||||||
# ls **/node_modules
|
# ls **/node_modules
|
||||||
|
ls **/*.bak
|
||||||
|
ls **/*draft
|
||||||
|
ls **/*.del
|
||||||
|
ls **/*.plan
|
||||||
ls **/*Zone*
|
ls **/*Zone*
|
||||||
|
|
||||||
set -e
|
# set -e
|
||||||
|
|
||||||
read -p "Press [Enter] key to clean directories..."
|
# read -p "Press [Enter] key to clean directories..."
|
||||||
|
|
||||||
# rm -rf **/.next
|
# # rm -rf **/.next
|
||||||
# rm -rf **/.pnpm
|
# # rm -rf **/.pnpm
|
||||||
# rm -rf **/node_modules
|
# # rm -rf **/node_modules
|
||||||
rm -rf **/*Zone*
|
# rm -rf **/*Zone*
|
||||||
|
|
||||||
echo "clean done"
|
echo "clean done"
|
||||||
|
39
002_source/cms/scripts/update_repomix.js
Normal file
39
002_source/cms/scripts/update_repomix.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
const exec = require('child_process').exec;
|
||||||
|
|
||||||
|
let directories = [
|
||||||
|
//
|
||||||
|
// './src/db',
|
||||||
|
// './src/app/dashboard/lp',
|
||||||
|
// './src/app/dashboard/mf',
|
||||||
|
// './src/app/dashboard/cr',
|
||||||
|
// './src/components/dashboard/lp',
|
||||||
|
// './src/components/dashboard/mf',
|
||||||
|
// './src/components/dashboard/cr',
|
||||||
|
// './src/app/dashboard/Sample',
|
||||||
|
'./src/components/dashboard/customer',
|
||||||
|
].map((directory) => {
|
||||||
|
return `cd ${directory} && pnpx repomix -c /home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/repomix.config.json`;
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
directories.map(
|
||||||
|
(directory) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
exec(directory, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
console.log(stdout);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
console.log('done');
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
// process.exit(1);
|
||||||
|
});
|
35
002_source/cms/src/__tests__/__snapshots__/snapshot.js.snap
Normal file
35
002_source/cms/src/__tests__/__snapshots__/snapshot.js.snap
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`renders homepage unchanged 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="MuiBox-root css-0"
|
||||||
|
>
|
||||||
|
hello world!
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="MuiBox-root css-0"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="MuiTypography-root MuiTypography-body1 css-1kivl2a-MuiTypography-root"
|
||||||
|
>
|
||||||
|
inner component
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="MuiListItemIcon-root css-cveggr-MuiListItemIcon-root"
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
21
002_source/cms/src/__tests__/app/_helloworld/page.test.tsx
Normal file
21
002_source/cms/src/__tests__/app/_helloworld/page.test.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/// <reference types="jest" />
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import Page from '@/app/_helloworld/page';
|
||||||
|
|
||||||
|
// Mock the translation hook
|
||||||
|
jest.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key: string) => key,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Page', () => {
|
||||||
|
it('renders a heading', () => {
|
||||||
|
render(<Page hello={'world'} />);
|
||||||
|
|
||||||
|
const heading = screen.getByRole('heading', { level: 1 });
|
||||||
|
|
||||||
|
expect(heading).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
9
002_source/cms/src/__tests__/snapshot.js
Normal file
9
002_source/cms/src/__tests__/snapshot.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
|
// CUT = Component Under Test
|
||||||
|
import CUT from '../components/_helloworld';
|
||||||
|
|
||||||
|
it('renders homepage unchanged', () => {
|
||||||
|
const { container } = render(<CUT />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
@@ -12,9 +12,9 @@ import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import type { Address } from '@/types/Address';
|
import type { Address } from '@/types/Address';
|
||||||
import { ShippingAddress } from '@/components/dashboard/lp_categories/shipping-address';
|
import { ShippingAddress } from '@/components/dashboard/lp/categories/shipping-address';
|
||||||
|
|
||||||
import { SampleAddresses } from '../SampleAddresses';
|
import { SampleAddresses } from './SampleAddresses';
|
||||||
|
|
||||||
export default function SampleAddressCard(): React.JSX.Element {
|
export default function SampleAddressCard(): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -22,7 +22,10 @@ export default function SampleAddressCard(): React.JSX.Element {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
action={
|
action={
|
||||||
<Button color="secondary" startIcon={<PlusIcon />}>
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<PlusIcon />}
|
||||||
|
>
|
||||||
{t('list.add')}
|
{t('list.add')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -34,9 +37,16 @@ export default function SampleAddressCard(): React.JSX.Element {
|
|||||||
title={t('list.shipping-addresses')}
|
title={t('list.shipping-addresses')}
|
||||||
/>
|
/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Grid container spacing={3}>
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={3}
|
||||||
|
>
|
||||||
{(SampleAddresses satisfies Address[]).map((address) => (
|
{(SampleAddresses satisfies Address[]).map((address) => (
|
||||||
<Grid key={address.id} md={6} xs={12}>
|
<Grid
|
||||||
|
key={address.id}
|
||||||
|
md={6}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
<ShippingAddress address={address} />
|
<ShippingAddress address={address} />
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
|
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import { PropertyItem } from '@/components/core/property-item';
|
import { PropertyItem } from '@/components/core/property-item';
|
||||||
import { PropertyList } from '@/components/core/property-list';
|
import { PropertyList } from '@/components/core/property-list';
|
||||||
import { Payments } from '@/components/dashboard/lp_categories/payments';
|
import { Payments } from '@/components/dashboard/lp/categories/payments';
|
||||||
|
|
||||||
import { SamplePayments } from './SamplePayments';
|
import { SamplePayments } from './SamplePayments';
|
||||||
|
|
||||||
@@ -21,11 +21,19 @@ export default function SamplePaymentCard(): React.JSX.Element {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Payments ordersValue={2069.48} payments={SamplePayments} refundsValue={324.5} totalOrders={5} />
|
<Payments
|
||||||
|
ordersValue={2069.48}
|
||||||
|
payments={SamplePayments}
|
||||||
|
refundsValue={324.5}
|
||||||
|
totalOrders={5}
|
||||||
|
/>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
action={
|
action={
|
||||||
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<PencilSimpleIcon />}
|
||||||
|
>
|
||||||
{t('list.edit')}
|
{t('list.edit')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -37,8 +45,14 @@ export default function SamplePaymentCard(): React.JSX.Element {
|
|||||||
title={t('list.billing-details')}
|
title={t('list.billing-details')}
|
||||||
/>
|
/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Card sx={{ borderRadius: 1 }} variant="outlined">
|
<Card
|
||||||
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}>
|
sx={{ borderRadius: 1 }}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<PropertyList
|
||||||
|
divider={<Divider />}
|
||||||
|
sx={{ '--PropertyItem-padding': '16px' }}
|
||||||
|
>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
{ key: t('Credit card'), value: '**** 4142' },
|
{ key: t('Credit card'), value: '**** 4142' },
|
||||||
@@ -50,7 +64,11 @@ export default function SamplePaymentCard(): React.JSX.Element {
|
|||||||
] satisfies { key: string; value: React.ReactNode }[]
|
] satisfies { key: string; value: React.ReactNode }[]
|
||||||
).map(
|
).map(
|
||||||
(item): React.JSX.Element => (
|
(item): React.JSX.Element => (
|
||||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
<PropertyItem
|
||||||
|
key={item.key}
|
||||||
|
name={item.key}
|
||||||
|
value={item.value}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</PropertyList>
|
</PropertyList>
|
@@ -27,11 +27,17 @@ export default function SampleSecurityCard(): React.JSX.Element {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
<div>
|
<div>
|
||||||
<Button color="error" variant="contained">
|
<Button
|
||||||
|
color="error"
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
{t('Delete account')}
|
{t('Delete account')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Typography color="text.secondary" variant="body2">
|
<Typography
|
||||||
|
color="text.secondary"
|
||||||
|
variant="body2"
|
||||||
|
>
|
||||||
{t('a-deleted-customer-cannot-be-restored-all-data-will-be-permanently-removed')}
|
{t('a-deleted-customer-cannot-be-restored-all-data-will-be-permanently-removed')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
1120
002_source/cms/src/app/dashboard/Sample/_repomix.md
Normal file
1120
002_source/cms/src/app/dashboard/Sample/_repomix.md
Normal file
File diff suppressed because it is too large
Load Diff
558
002_source/cms/src/app/dashboard/Sample/repomix-output.xml
Normal file
558
002_source/cms/src/app/dashboard/Sample/repomix-output.xml
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
This file is a merged representation of the entire codebase, combined into a single document by Repomix.
|
||||||
|
|
||||||
|
<file_summary>
|
||||||
|
This section contains a summary of this file.
|
||||||
|
|
||||||
|
<purpose>
|
||||||
|
This file contains a packed representation of the entire repository's contents.
|
||||||
|
It is designed to be easily consumable by AI systems for analysis, code review,
|
||||||
|
or other automated processes.
|
||||||
|
</purpose>
|
||||||
|
|
||||||
|
<file_format>
|
||||||
|
The content is organized as follows:
|
||||||
|
1. This summary section
|
||||||
|
2. Repository information
|
||||||
|
3. Directory structure
|
||||||
|
4. Repository files, each consisting of:
|
||||||
|
- File path as an attribute
|
||||||
|
- Full contents of the file
|
||||||
|
</file_format>
|
||||||
|
|
||||||
|
<usage_guidelines>
|
||||||
|
- This file should be treated as read-only. Any changes should be made to the
|
||||||
|
original repository files, not this packed version.
|
||||||
|
- When processing this file, use the file path to distinguish
|
||||||
|
between different files in the repository.
|
||||||
|
- Be aware that this file may contain sensitive information. Handle it with
|
||||||
|
the same level of security as you would the original repository.
|
||||||
|
</usage_guidelines>
|
||||||
|
|
||||||
|
<notes>
|
||||||
|
- Some files may have been excluded based on .gitignore rules and Repomix's configuration
|
||||||
|
- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files
|
||||||
|
- Files matching patterns in .gitignore are excluded
|
||||||
|
- Files matching default ignore patterns are excluded
|
||||||
|
- Files are sorted by Git change count (files with more changes are at the bottom)
|
||||||
|
</notes>
|
||||||
|
|
||||||
|
<additional_info>
|
||||||
|
|
||||||
|
</additional_info>
|
||||||
|
|
||||||
|
</file_summary>
|
||||||
|
|
||||||
|
<directory_structure>
|
||||||
|
AddressCard/
|
||||||
|
index.tsx
|
||||||
|
SampleAddresses.tsx
|
||||||
|
BasicDetailCard/
|
||||||
|
index.tsx
|
||||||
|
Notifications/
|
||||||
|
index.tsx
|
||||||
|
type.d.ts
|
||||||
|
PaymentCard/
|
||||||
|
index.tsx
|
||||||
|
SamplePayments.tsx
|
||||||
|
SecurityCard/
|
||||||
|
index.tsx
|
||||||
|
TitleCard/
|
||||||
|
index.tsx
|
||||||
|
Helloworld.tsx
|
||||||
|
</directory_structure>
|
||||||
|
|
||||||
|
<files>
|
||||||
|
This section contains the contents of the repository's files.
|
||||||
|
|
||||||
|
<file path="AddressCard/index.tsx">
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import CardHeader from '@mui/material/CardHeader';
|
||||||
|
import Grid from '@mui/material/Unstable_Grid2';
|
||||||
|
import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House';
|
||||||
|
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import type { Address } from '@/types/Address';
|
||||||
|
import { ShippingAddress } from '@/components/dashboard/lp/categories/shipping-address';
|
||||||
|
|
||||||
|
import { SampleAddresses } from './SampleAddresses';
|
||||||
|
|
||||||
|
export default function SampleAddressCard(): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<PlusIcon />}
|
||||||
|
>
|
||||||
|
{t('list.add')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
avatar={
|
||||||
|
<Avatar>
|
||||||
|
<HouseIcon fontSize="var(--Icon-fontSize)" />
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
title={t('list.shipping-addresses')}
|
||||||
|
/>
|
||||||
|
<CardContent>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={3}
|
||||||
|
>
|
||||||
|
{(SampleAddresses satisfies Address[]).map((address) => (
|
||||||
|
<Grid
|
||||||
|
key={address.id}
|
||||||
|
md={6}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
|
<ShippingAddress address={address} />
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<file path="AddressCard/SampleAddresses.tsx">
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { Address } from '@/types/Address';
|
||||||
|
|
||||||
|
export const SampleAddresses: Address[] = [
|
||||||
|
{
|
||||||
|
id: 'ADR-001',
|
||||||
|
country: 'United States',
|
||||||
|
state: 'Michigan',
|
||||||
|
city: 'Lansing',
|
||||||
|
zipCode: '48933',
|
||||||
|
street: '480 Haven Lane',
|
||||||
|
primary: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ADR-002',
|
||||||
|
country: 'United States',
|
||||||
|
state: 'Missouri',
|
||||||
|
city: 'Springfield',
|
||||||
|
zipCode: '65804',
|
||||||
|
street: '4807 Lighthouse Drive',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<file path="BasicDetailCard/index.tsx">
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardHeader from '@mui/material/CardHeader';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import LinearProgress from '@mui/material/LinearProgress';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||||
|
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { PropertyItem } from '@/components/core/property-item';
|
||||||
|
import { PropertyList } from '@/components/core/property-list';
|
||||||
|
|
||||||
|
export default function BasicDetailCard({
|
||||||
|
lpCatId,
|
||||||
|
handleEditClick,
|
||||||
|
}: {
|
||||||
|
lpCatId: string;
|
||||||
|
handleEditClick: () => void;
|
||||||
|
}): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
action={
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
handleEditClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PencilSimpleIcon />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
avatar={
|
||||||
|
<Avatar>
|
||||||
|
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
title={t('list.basic-details')}
|
||||||
|
/>
|
||||||
|
<PropertyList
|
||||||
|
divider={<Divider />}
|
||||||
|
orientation="vertical"
|
||||||
|
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
||||||
|
>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: 'Customer ID',
|
||||||
|
value: (
|
||||||
|
<Chip
|
||||||
|
label="USR-001"
|
||||||
|
size="small"
|
||||||
|
variant="soft"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ key: 'Name', value: 'Miron Vitold' },
|
||||||
|
{ key: 'Email', value: 'miron.vitold@domain.com' },
|
||||||
|
{ key: 'Phone', value: '(425) 434-5535' },
|
||||||
|
{ key: 'Company', value: 'Devias IO' },
|
||||||
|
{
|
||||||
|
key: 'Quota',
|
||||||
|
value: (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
<LinearProgress
|
||||||
|
sx={{ flex: '1 1 auto' }}
|
||||||
|
value={50}
|
||||||
|
variant="determinate"
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
color="text.secondary"
|
||||||
|
variant="body2"
|
||||||
|
>
|
||||||
|
50%
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
] satisfies { key: string; value: React.ReactNode }[]
|
||||||
|
).map(
|
||||||
|
(item): React.JSX.Element => (
|
||||||
|
<PropertyItem
|
||||||
|
key={item.key}
|
||||||
|
name={item.key}
|
||||||
|
value={item.value}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</PropertyList>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<file path="Notifications/index.tsx">
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { dayjs } from '@/lib/dayjs';
|
||||||
|
|
||||||
|
import type { Notification } from './type';
|
||||||
|
|
||||||
|
export const SampleNotifications: Notification[] = [
|
||||||
|
{
|
||||||
|
id: 'EV-002',
|
||||||
|
type: 'Refund request approved',
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: dayjs().subtract(34, 'minute').subtract(5, 'hour').subtract(3, 'day').toDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'EV-001',
|
||||||
|
type: 'Order confirmation',
|
||||||
|
status: 'delivered',
|
||||||
|
createdAt: dayjs().subtract(49, 'minute').subtract(11, 'hour').subtract(4, 'day').toDate(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<file path="Notifications/type.d.ts">
|
||||||
|
export interface Notification {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
status: 'delivered' | 'pending' | 'failed';
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<file path="PaymentCard/index.tsx">
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import CardHeader from '@mui/material/CardHeader';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard';
|
||||||
|
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { PropertyItem } from '@/components/core/property-item';
|
||||||
|
import { PropertyList } from '@/components/core/property-list';
|
||||||
|
import { Payments } from '@/components/dashboard/lp/categories/payments';
|
||||||
|
|
||||||
|
import { SamplePayments } from './SamplePayments';
|
||||||
|
|
||||||
|
export default function SamplePaymentCard(): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Payments
|
||||||
|
ordersValue={2069.48}
|
||||||
|
payments={SamplePayments}
|
||||||
|
refundsValue={324.5}
|
||||||
|
totalOrders={5}
|
||||||
|
/>
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<PencilSimpleIcon />}
|
||||||
|
>
|
||||||
|
{t('list.edit')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
avatar={
|
||||||
|
<Avatar>
|
||||||
|
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
title={t('list.billing-details')}
|
||||||
|
/>
|
||||||
|
<CardContent>
|
||||||
|
<Card
|
||||||
|
sx={{ borderRadius: 1 }}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<PropertyList
|
||||||
|
divider={<Divider />}
|
||||||
|
sx={{ '--PropertyItem-padding': '16px' }}
|
||||||
|
>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{ key: t('Credit card'), value: '**** 4142' },
|
||||||
|
{ key: t('Country'), value: t('United States') },
|
||||||
|
{ key: t('State'), value: t('Michigan') },
|
||||||
|
{ key: t('City'), value: t('Southfield') },
|
||||||
|
{ key: t('Address'), value: t('Address') },
|
||||||
|
{ key: t('Tax ID'), value: t('Tax ID') },
|
||||||
|
] satisfies { key: string; value: React.ReactNode }[]
|
||||||
|
).map(
|
||||||
|
(item): React.JSX.Element => (
|
||||||
|
<PropertyItem
|
||||||
|
key={item.key}
|
||||||
|
name={item.key}
|
||||||
|
value={item.value}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</PropertyList>
|
||||||
|
</Card>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<file path="PaymentCard/SamplePayments.tsx">
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
// import { dayjs } from 'dayjs';
|
||||||
|
import type { Payment } from '@/types/Payment';
|
||||||
|
import { dayjs } from '@/lib/dayjs';
|
||||||
|
|
||||||
|
export const SamplePayments: Payment[] = [
|
||||||
|
{
|
||||||
|
currency: 'USD',
|
||||||
|
amount: 500,
|
||||||
|
invoiceId: 'INV-005',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: dayjs().subtract(5, 'minute').subtract(1, 'hour').toDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency: 'USD',
|
||||||
|
amount: 324.5,
|
||||||
|
invoiceId: 'INV-004',
|
||||||
|
status: 'refunded',
|
||||||
|
createdAt: dayjs().subtract(21, 'minute').subtract(2, 'hour').toDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency: 'USD',
|
||||||
|
amount: 746.5,
|
||||||
|
invoiceId: 'INV-003',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: dayjs().subtract(7, 'minute').subtract(3, 'hour').toDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency: 'USD',
|
||||||
|
amount: 56.89,
|
||||||
|
invoiceId: 'INV-002',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: dayjs().subtract(48, 'minute').subtract(4, 'hour').toDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency: 'USD',
|
||||||
|
amount: 541.59,
|
||||||
|
invoiceId: 'INV-001',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: dayjs().subtract(31, 'minute').subtract(5, 'hour').toDate(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<file path="SecurityCard/index.tsx">
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import CardHeader from '@mui/material/CardHeader';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export default function SampleSecurityCard(): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
avatar={
|
||||||
|
<Avatar>
|
||||||
|
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
title={t('list.security')}
|
||||||
|
/>
|
||||||
|
<CardContent>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
color="error"
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
{t('Delete account')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Typography
|
||||||
|
color="text.secondary"
|
||||||
|
variant="body2"
|
||||||
|
>
|
||||||
|
{t('a-deleted-customer-cannot-be-restored-all-data-will-be-permanently-removed')}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<file path="TitleCard/index.tsx">
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||||
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export default function SampleTitleCard(): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center', flex: '1 1 auto' }}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
src="/assets/avatar-1.png"
|
||||||
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
|
>
|
||||||
|
empty
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
|
||||||
|
>
|
||||||
|
<Typography variant="h4">{t('list.customer-name')}</Typography>
|
||||||
|
<Chip
|
||||||
|
icon={
|
||||||
|
<CheckCircleIcon
|
||||||
|
color="var(--mui-palette-success-main)"
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t('list.active')}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Typography
|
||||||
|
color="text.secondary"
|
||||||
|
variant="body1"
|
||||||
|
>
|
||||||
|
{t('list.customer-email')}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
endIcon={<CaretDownIcon />}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
{t('list.action')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<file path="Helloworld.tsx">
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
|
||||||
|
|
||||||
|
function Page(): React.JSX.Element {
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
console.log('helloworld');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <>helloworld</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
</file>
|
||||||
|
|
||||||
|
</files>
|
104
002_source/cms/src/app/dashboard/cr/_GUIDELINES.md
Normal file
104
002_source/cms/src/app/dashboard/cr/_GUIDELINES.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# Connective Revision Guidelines
|
||||||
|
|
||||||
|
## Files and component highlight
|
||||||
|
|
||||||
|
1. `_GUIDELINES.md` - this document
|
||||||
|
1. categories
|
||||||
|
|
||||||
|
- list (page.tsx), also containing a button to delete record
|
||||||
|
- read/view ([cat_id]/page.tsx)
|
||||||
|
- create (create/page.tsx)
|
||||||
|
- edit/update (edit/[cat_id]/page.tsx)
|
||||||
|
- optional data for testing(lp-categories-sample-data.tsx)
|
||||||
|
|
||||||
|
1. questions
|
||||||
|
|
||||||
|
- list (page.tsx), also containing a button to delete record
|
||||||
|
- read/view ([cat_id]/page.tsx)
|
||||||
|
- create (create/page.tsx)
|
||||||
|
- edit/update (edit/[cat_id]/page.tsx)
|
||||||
|
- optional data for testing(lp-categories-sample-data.tsx)
|
||||||
|
|
||||||
|
## Prompt Documents
|
||||||
|
|
||||||
|
Each edit page contains `_PROMPT.md` file that provides guidance for editing.
|
||||||
|
|
||||||
|
## Sample Data
|
||||||
|
|
||||||
|
- `categories/lp-categories-sample-data.tsx`: Categories sample data
|
||||||
|
- `questions/cr-categories-sample-data.tsx`: Questions sample data
|
||||||
|
|
||||||
|
## Assumptions & Requirements
|
||||||
|
|
||||||
|
1. Using PocketBase to handle ConnectiveRevision records
|
||||||
|
2. Each ConnectiveRevision record has:
|
||||||
|
- `id` (autogenerated)
|
||||||
|
- `collectionId` (autogenerated)
|
||||||
|
- `collectionName` (autogenerated)
|
||||||
|
- `created` (autogenerated)
|
||||||
|
- `updated` (autogenerated)
|
||||||
|
- `title` (string)
|
||||||
|
- `description` (string)
|
||||||
|
- `category` (string)
|
||||||
|
- `status` (string)
|
||||||
|
- `priority` (number)
|
||||||
|
- `dueDate` (string)
|
||||||
|
- `assignee` (string)
|
||||||
|
- `reporter` (string)
|
||||||
|
- `comments` (array)
|
||||||
|
- `attachments` (array)
|
||||||
|
- `tags` (array)
|
||||||
|
- `related` (array)
|
||||||
|
- `history` (array)
|
||||||
|
|
||||||
|
## Assumption and Requirements
|
||||||
|
|
||||||
|
- the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src`
|
||||||
|
- assume `pb` is located in `@/lib/pb`
|
||||||
|
- 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`
|
||||||
|
|
||||||
|
## Component Development Guidelines
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
1. **Single Responsibility Principle**:
|
||||||
|
|
||||||
|
2. **File Organization**:
|
||||||
|
|
||||||
|
- One file per component
|
||||||
|
- File name should match component name (PascalCase)
|
||||||
|
- Place components in logical directories based on their purpose
|
||||||
|
|
||||||
|
3. **Type Safety**:
|
||||||
|
|
||||||
|
- Always use TypeScript types/interfaces
|
||||||
|
- Import types from `@/db/Customers/type.d.tsx`
|
||||||
|
|
||||||
|
4. **PocketBase Integration**:
|
||||||
|
- Use `pb` instance from `@/lib/pb`
|
||||||
|
|
||||||
|
### Component Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { pb } from '@/lib/pb';
|
||||||
|
import { COL_ExampleModel } from '@/constants';
|
||||||
|
import type { ExampleType } from '@/db/ExampleModel/type';
|
||||||
|
|
||||||
|
// common reference to error display, Provide user-friendly error messages
|
||||||
|
import ErrorDisplay from '@/components/dashboard/error';
|
||||||
|
|
||||||
|
// declare `Props` explicitively
|
||||||
|
interface Props {
|
||||||
|
initialData?: ExampleType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExampleForm({ initialData }: Props) {
|
||||||
|
let { t } = useTranslate();
|
||||||
|
|
||||||
|
// Render form UI
|
||||||
|
return <>helloworld</>
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
3555
002_source/cms/src/app/dashboard/cr/_repomix.md
Normal file
3555
002_source/cms/src/app/dashboard/cr/_repomix.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardHeader from '@mui/material/CardHeader';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||||
|
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { PropertyItem } from '@/components/core/property-item';
|
||||||
|
import { PropertyList } from '@/components/core/property-list';
|
||||||
|
import { CrCategory } from '@/components/dashboard/cr/categories/type';
|
||||||
|
|
||||||
|
export default function BasicDetailCard({
|
||||||
|
lpModel: model,
|
||||||
|
handleEditClick,
|
||||||
|
}: {
|
||||||
|
lpModel: CrCategory;
|
||||||
|
handleEditClick: () => void;
|
||||||
|
}): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
action={
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
handleEditClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PencilSimpleIcon />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
avatar={
|
||||||
|
<Avatar>
|
||||||
|
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
title={t('list.basic-details')}
|
||||||
|
/>
|
||||||
|
<PropertyList
|
||||||
|
divider={<Divider />}
|
||||||
|
orientation="vertical"
|
||||||
|
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
||||||
|
>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: 'Customer ID',
|
||||||
|
value: (
|
||||||
|
<Chip
|
||||||
|
label={model.id}
|
||||||
|
size="small"
|
||||||
|
variant="soft"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ key: 'Name', value: model.cat_name },
|
||||||
|
{ key: 'Remarks', value: model.remarks },
|
||||||
|
{ key: 'Description', value: model.description },
|
||||||
|
] satisfies { key: string; value: React.ReactNode }[]
|
||||||
|
).map(
|
||||||
|
(item): React.JSX.Element => (
|
||||||
|
<PropertyItem
|
||||||
|
key={item.key}
|
||||||
|
name={item.key}
|
||||||
|
value={item.value}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</PropertyList>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,73 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||||
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import type { CrCategory } from '@/components/dashboard/cr/categories/type';
|
||||||
|
|
||||||
|
function getImageUrlFrRecord(record: CrCategory): string {
|
||||||
|
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SampleTitleCard({ lpModel }: { lpModel: CrCategory }): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center', flex: '1 1 auto' }}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
variant="rounded"
|
||||||
|
src={getImageUrlFrRecord(lpModel)}
|
||||||
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
|
>
|
||||||
|
{t('empty')}
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
|
||||||
|
>
|
||||||
|
<Typography variant="h4">{lpModel.cat_name}</Typography>
|
||||||
|
<Chip
|
||||||
|
icon={
|
||||||
|
<CheckCircleIcon
|
||||||
|
color="var(--mui-palette-success-main)"
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={lpModel.visible}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Typography
|
||||||
|
color="text.secondary"
|
||||||
|
variant="body1"
|
||||||
|
>
|
||||||
|
{lpModel.slug}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
endIcon={<CaretDownIcon />}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
{t('list.action')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
135
002_source/cms/src/app/dashboard/cr/categories/[cat_id]/page.tsx
Normal file
135
002_source/cms/src/app/dashboard/cr/categories/[cat_id]/page.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import RouterLink from 'next/link';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
|
||||||
|
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
|
||||||
|
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
|
||||||
|
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
|
||||||
|
import { COL_QUIZ_CR_CATEGORIES } from '@/constants';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Grid from '@mui/material/Unstable_Grid2';
|
||||||
|
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||||
|
import type { RecordModel } from 'pocketbase';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { paths } from '@/paths';
|
||||||
|
import { logger } from '@/lib/default-logger';
|
||||||
|
import { pb } from '@/lib/pb';
|
||||||
|
import { toast } from '@/components/core/toaster';
|
||||||
|
import ErrorDisplay from '@/components/dashboard/error';
|
||||||
|
import { defaultCrCategory } from '@/components/dashboard/cr/categories/_constants.ts';
|
||||||
|
import { Notifications } from '@/components/dashboard/cr/categories/notifications';
|
||||||
|
import type { CrCategory } from '@/components/dashboard/cr/categories/type';
|
||||||
|
import FormLoading from '@/components/loading';
|
||||||
|
import BasicDetailCard from './BasicDetailCard';
|
||||||
|
import TitleCard from './TitleCard';
|
||||||
|
|
||||||
|
export default function Page(): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
//
|
||||||
|
const { cat_id: catId } = useParams<{ cat_id: string }>();
|
||||||
|
//
|
||||||
|
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||||
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||||
|
//
|
||||||
|
const [showLessonCategory, setShowLessonCategory] = React.useState<CrCategory>(defaultCrCategory);
|
||||||
|
|
||||||
|
function handleEditClick() {
|
||||||
|
router.push(paths.dashboard.cr_categories.edit(showLessonCategory.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (catId) {
|
||||||
|
pb.collection(COL_QUIZ_CR_CATEGORIES)
|
||||||
|
.getOne(catId)
|
||||||
|
.then((model: RecordModel) => {
|
||||||
|
setShowLessonCategory({ ...defaultCrCategory, ...model });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(err);
|
||||||
|
toast(t('list.error'));
|
||||||
|
setShowError({ show: true, detail: JSON.stringify(err) });
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setShowLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [catId]);
|
||||||
|
|
||||||
|
if (showLoading) return <FormLoading />;
|
||||||
|
if (showError.show)
|
||||||
|
return (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t('error.unable-to-process-request')}
|
||||||
|
code="500"
|
||||||
|
details={showError.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxWidth: 'var(--Content-maxWidth)',
|
||||||
|
m: 'var(--Content-margin)',
|
||||||
|
p: 'var(--Content-padding)',
|
||||||
|
width: 'var(--Content-width)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
color="text.primary"
|
||||||
|
component={RouterLink}
|
||||||
|
href={paths.dashboard.cr_categories.list}
|
||||||
|
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||||
|
variant="subtitle2"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||||
|
{t('list.title')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
|
spacing={3}
|
||||||
|
sx={{ alignItems: 'flex-start' }}
|
||||||
|
>
|
||||||
|
<TitleCard lpModel={showLessonCategory} />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={4}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
lg={4}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<BasicDetailCard
|
||||||
|
lpModel={showLessonCategory}
|
||||||
|
handleEditClick={handleEditClick}
|
||||||
|
/>
|
||||||
|
<SampleSecurityCard />
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
lg={8}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<SamplePaymentCard />
|
||||||
|
<SampleAddressCard />
|
||||||
|
<Notifications notifications={SampleNotifications} />
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,53 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
// RULES:
|
||||||
|
// T.B.A.
|
||||||
|
//
|
||||||
|
import * as React from 'react';
|
||||||
|
import RouterLink from 'next/link';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { paths } from '@/paths';
|
||||||
|
import { CrCategoryCreateForm } from '@/components/dashboard/cr/categories/cr-category-create-form';
|
||||||
|
|
||||||
|
export default function Page(): React.JSX.Element {
|
||||||
|
// RULES: follow the name of page directory e.g. cr/categoies -> cr_categories
|
||||||
|
const { t } = useTranslation(['lp_categories']);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxWidth: 'var(--Content-maxWidth)',
|
||||||
|
m: 'var(--Content-margin)',
|
||||||
|
p: 'var(--Content-padding)',
|
||||||
|
width: 'var(--Content-width)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
color="text.primary"
|
||||||
|
component={RouterLink}
|
||||||
|
href={paths.dashboard.cr_categories.list}
|
||||||
|
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||||
|
variant="subtitle2"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||||
|
{t('title')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="h4">{t('create.title')}</Typography>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
<CrCategoryCreateForm />
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
# task
|
||||||
|
|
||||||
|
## instruction
|
||||||
|
|
||||||
|
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/_helloworld/page.tsx`
|
||||||
|
|
||||||
|
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_types/edit/[typeId]/page.tsx`
|
||||||
|
|
||||||
|
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_categories/edit/page.tsx`
|
||||||
|
|
||||||
|
please draft a tsx for showing error to user thanks,
|
@@ -0,0 +1,53 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import RouterLink from 'next/link';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { paths } from '@/paths';
|
||||||
|
import { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form';
|
||||||
|
|
||||||
|
export default function Page(): React.JSX.Element {
|
||||||
|
const { t } = useTranslation(['lp_categories']);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// console.log('helloworld');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxWidth: 'var(--Content-maxWidth)',
|
||||||
|
m: 'var(--Content-margin)',
|
||||||
|
p: 'var(--Content-padding)',
|
||||||
|
width: 'var(--Content-width)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
color="text.primary"
|
||||||
|
component={RouterLink}
|
||||||
|
href={paths.dashboard.cr_categories.list}
|
||||||
|
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||||
|
variant="subtitle2"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||||
|
{t('edit.title')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="h4">{t('edit.title')}</Typography>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
<CrCategoryEditForm />
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,90 @@
|
|||||||
|
import { dayjs } from '@/lib/dayjs';
|
||||||
|
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||||
|
|
||||||
|
export const LpCategoriesSampleData = [
|
||||||
|
{
|
||||||
|
id: 'USR-005',
|
||||||
|
name: 'Fran Perez',
|
||||||
|
avatar: '/assets/avatar-5.png',
|
||||||
|
email: 'fran.perez@domain.com',
|
||||||
|
phone: '(815) 704-0045',
|
||||||
|
quota: 50,
|
||||||
|
status: 'active',
|
||||||
|
createdAt: dayjs().subtract(1, 'hour').toDate(),
|
||||||
|
collectionId: '0000000001',
|
||||||
|
cat_name: '',
|
||||||
|
pos: 99,
|
||||||
|
visible: 'visible',
|
||||||
|
lesson_id: 'lid_00001',
|
||||||
|
description: '',
|
||||||
|
remarks: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'USR-004',
|
||||||
|
name: 'Penjani Inyene',
|
||||||
|
avatar: '/assets/avatar-4.png',
|
||||||
|
email: 'penjani.inyene@domain.com',
|
||||||
|
phone: '(803) 937-8925',
|
||||||
|
quota: 100,
|
||||||
|
status: 'active',
|
||||||
|
createdAt: dayjs().subtract(3, 'hour').toDate(),
|
||||||
|
collectionId: '0000000001',
|
||||||
|
cat_name: '',
|
||||||
|
pos: 99,
|
||||||
|
visible: 'visible',
|
||||||
|
lesson_id: 'lid_00001',
|
||||||
|
description: '',
|
||||||
|
remarks: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'USR-003',
|
||||||
|
name: 'Carson Darrin',
|
||||||
|
avatar: '/assets/avatar-3.png',
|
||||||
|
email: 'carson.darrin@domain.com',
|
||||||
|
phone: '(715) 278-5041',
|
||||||
|
quota: 10,
|
||||||
|
status: 'blocked',
|
||||||
|
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
|
||||||
|
collectionId: '0000000001',
|
||||||
|
cat_name: '',
|
||||||
|
pos: 99,
|
||||||
|
visible: 'visible',
|
||||||
|
lesson_id: 'lid_00001',
|
||||||
|
description: '',
|
||||||
|
remarks: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'USR-002',
|
||||||
|
name: 'Siegbert Gottfried',
|
||||||
|
avatar: '/assets/avatar-2.png',
|
||||||
|
email: 'siegbert.gottfried@domain.com',
|
||||||
|
phone: '(603) 766-0431',
|
||||||
|
quota: 0,
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
|
||||||
|
collectionId: '0000000001',
|
||||||
|
cat_name: '',
|
||||||
|
pos: 99,
|
||||||
|
visible: 'visible',
|
||||||
|
lesson_id: 'lid_00001',
|
||||||
|
description: '',
|
||||||
|
remarks: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'USR-001',
|
||||||
|
name: 'Miron Vitold',
|
||||||
|
avatar: '/assets/avatar-1.png',
|
||||||
|
email: 'miron.vitold@domain.com',
|
||||||
|
phone: '(425) 434-5535',
|
||||||
|
quota: 50,
|
||||||
|
status: 'active',
|
||||||
|
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
||||||
|
collectionId: '0000000001',
|
||||||
|
cat_name: '',
|
||||||
|
pos: 99,
|
||||||
|
visible: 'visible',
|
||||||
|
lesson_id: 'lid_00001',
|
||||||
|
description: '',
|
||||||
|
remarks: '',
|
||||||
|
},
|
||||||
|
] satisfies LessonCategory[];
|
276
002_source/cms/src/app/dashboard/cr/categories/page.tsx
Normal file
276
002_source/cms/src/app/dashboard/cr/categories/page.tsx
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
// RULES:
|
||||||
|
// contains list page for cr_categories (QuizCRCategories)
|
||||||
|
// contain definition to collection only
|
||||||
|
//
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { COL_QUIZ_CR_CATEGORIES } from '@/constants';
|
||||||
|
import { LoadingButton } from '@mui/lab';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||||
|
import type { ListResult, RecordModel } from 'pocketbase';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { paths } from '@/paths';
|
||||||
|
import isDevelopment from '@/lib/check-is-development';
|
||||||
|
import { logger } from '@/lib/default-logger';
|
||||||
|
import { pb } from '@/lib/pb';
|
||||||
|
import { toast } from '@/components/core/toaster';
|
||||||
|
import { defaultCrCategory } from '@/components/dashboard/cr/categories/_constants';
|
||||||
|
import { CrCategoriesFilters } from '@/components/dashboard/cr/categories/cr-categories-filters';
|
||||||
|
import type { Filters } from '@/components/dashboard/cr/categories/cr-categories-filters';
|
||||||
|
import { CrCategoriesPagination } from '@/components/dashboard/cr/categories/cr-categories-pagination';
|
||||||
|
import { CrCategoriesSelectionProvider } from '@/components/dashboard/cr/categories/cr-categories-selection-context';
|
||||||
|
import { CrCategoriesTable } from '@/components/dashboard/cr/categories/cr-categories-table';
|
||||||
|
import type { CrCategory } from '@/components/dashboard/cr/categories/type';
|
||||||
|
import ErrorDisplay from '@/components/dashboard/error';
|
||||||
|
import FormLoading from '@/components/loading';
|
||||||
|
|
||||||
|
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||||
|
const { t } = useTranslation(['lp_categories']);
|
||||||
|
// TODO: align to customers page.tsx
|
||||||
|
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||||
|
const router = useRouter();
|
||||||
|
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<CrCategory[]>([]);
|
||||||
|
//
|
||||||
|
|
||||||
|
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
||||||
|
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||||
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||||
|
//
|
||||||
|
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||||
|
//
|
||||||
|
const [f, setF] = React.useState<CrCategory[]>([]);
|
||||||
|
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||||
|
//
|
||||||
|
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||||
|
const [listOption, setListOption] = React.useState({});
|
||||||
|
const [listSort, setListSort] = React.useState({});
|
||||||
|
|
||||||
|
//
|
||||||
|
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
|
||||||
|
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status });
|
||||||
|
|
||||||
|
//
|
||||||
|
const reloadRows = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const models: ListResult<RecordModel> = await pb
|
||||||
|
.collection(COL_QUIZ_CR_CATEGORIES)
|
||||||
|
.getList(currentPage + 1, rowsPerPage, listOption);
|
||||||
|
const { items, totalItems } = models;
|
||||||
|
const tempLessonTypes: CrCategory[] = items.map((lt) => {
|
||||||
|
return { ...defaultCrCategory, ...lt };
|
||||||
|
});
|
||||||
|
|
||||||
|
setLessonCategoriesData(tempLessonTypes);
|
||||||
|
setRecordCount(totalItems);
|
||||||
|
setF(tempLessonTypes);
|
||||||
|
// console.log({ currentPage, f });
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
logger.error(error);
|
||||||
|
setShowError({
|
||||||
|
//
|
||||||
|
show: true,
|
||||||
|
detail: JSON.stringify(error, null, 2),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setShowLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [lastListOption, setLastListOption] = React.useState({});
|
||||||
|
const isFirstRun = React.useRef(false);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isFirstRun.current) {
|
||||||
|
isFirstRun.current = true;
|
||||||
|
} else {
|
||||||
|
if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
||||||
|
// reset page number as tab changes
|
||||||
|
setLastListOption(listOption);
|
||||||
|
setCurrentPage(0);
|
||||||
|
void reloadRows();
|
||||||
|
} else {
|
||||||
|
void reloadRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentPage, rowsPerPage, listOption]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let tempFilter = [],
|
||||||
|
tempSortDir = '';
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
tempFilter.push(`visible = "${visible}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDir) {
|
||||||
|
tempSortDir = `-created`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
tempFilter.push(`name ~ "%${name}%"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
tempFilter.push(`type ~ "%${type}%"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let preFinalListOption = {};
|
||||||
|
if (tempFilter.length > 0) {
|
||||||
|
preFinalListOption = { filter: tempFilter.join(' && ') };
|
||||||
|
}
|
||||||
|
if (tempSortDir.length > 0) {
|
||||||
|
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
|
||||||
|
}
|
||||||
|
setListOption(preFinalListOption);
|
||||||
|
// setListOption({
|
||||||
|
// filter: tempFilter.join(' && '),
|
||||||
|
// sort: tempSortDir,
|
||||||
|
// //
|
||||||
|
// });
|
||||||
|
}, [visible, sortDir, name, type]);
|
||||||
|
|
||||||
|
// return <>helloworld</>;
|
||||||
|
|
||||||
|
if (showLoading) return <FormLoading />;
|
||||||
|
|
||||||
|
if (showError.show)
|
||||||
|
return (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t('error.unable-to-process-request')}
|
||||||
|
code={-1}
|
||||||
|
details={showError.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxWidth: 'var(--Content-maxWidth)',
|
||||||
|
m: 'var(--Content-margin)',
|
||||||
|
p: 'var(--Content-padding)',
|
||||||
|
width: 'var(--Content-width)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
|
spacing={3}
|
||||||
|
sx={{ alignItems: 'flex-start' }}
|
||||||
|
>
|
||||||
|
<Box sx={{ flex: '1 1 auto' }}>
|
||||||
|
<Typography variant="h4">{t('list.title')}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<LoadingButton
|
||||||
|
loading={isLoadingAddPage}
|
||||||
|
onClick={(): void => {
|
||||||
|
setIsLoadingAddPage(true);
|
||||||
|
router.push(paths.dashboard.cr_categories.create);
|
||||||
|
}}
|
||||||
|
startIcon={<PlusIcon />}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
{t('list.add')}
|
||||||
|
</LoadingButton>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<CrCategoriesSelectionProvider lessonCategories={f}>
|
||||||
|
<Card>
|
||||||
|
<CrCategoriesFilters
|
||||||
|
filters={{ email, phone, status, name, visible, type }}
|
||||||
|
fullData={lessonCategoriesData}
|
||||||
|
sortDir={sortDir}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<Box sx={{ overflowX: 'auto' }}>
|
||||||
|
<CrCategoriesTable
|
||||||
|
reloadRows={reloadRows}
|
||||||
|
rows={f}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<CrCategoriesPagination
|
||||||
|
count={recordCount}
|
||||||
|
page={currentPage}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
setPage={setCurrentPage}
|
||||||
|
setRowsPerPage={setRowsPerPage}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</CrCategoriesSelectionProvider>
|
||||||
|
</Stack>
|
||||||
|
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
|
||||||
|
<pre>{JSON.stringify(f, null, 2)}</pre>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting and filtering has to be done on the server.
|
||||||
|
|
||||||
|
function applySort(row: CrCategory[], sortDir: 'asc' | 'desc' | undefined): CrCategory[] {
|
||||||
|
return row.sort((a, b) => {
|
||||||
|
if (sortDir === 'asc') {
|
||||||
|
return a.createdAt.getTime() - b.createdAt.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.createdAt.getTime() - a.createdAt.getTime();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilters(row: CrCategory[], { email, phone, status, name, visible }: Filters): CrCategory[] {
|
||||||
|
return row.filter((item) => {
|
||||||
|
if (email) {
|
||||||
|
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phone) {
|
||||||
|
if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
if (item.status !== status) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
if (!item.name?.toLowerCase().includes(name.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
searchParams: {
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
sortDir?: 'asc' | 'desc';
|
||||||
|
status?: string;
|
||||||
|
name?: string;
|
||||||
|
visible?: string;
|
||||||
|
type?: string;
|
||||||
|
//
|
||||||
|
};
|
||||||
|
}
|
@@ -0,0 +1,79 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardHeader from '@mui/material/CardHeader';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||||
|
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { PropertyItem } from '@/components/core/property-item';
|
||||||
|
import { PropertyList } from '@/components/core/property-list';
|
||||||
|
import { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||||
|
|
||||||
|
export default function BasicDetailCard({
|
||||||
|
lpModel: model,
|
||||||
|
handleEditClick,
|
||||||
|
}: {
|
||||||
|
lpModel: LpCategory;
|
||||||
|
handleEditClick: () => void;
|
||||||
|
}): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
action={
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
handleEditClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PencilSimpleIcon />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
avatar={
|
||||||
|
<Avatar>
|
||||||
|
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
title={t('list.basic-details')}
|
||||||
|
/>
|
||||||
|
<PropertyList
|
||||||
|
divider={<Divider />}
|
||||||
|
orientation="vertical"
|
||||||
|
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
||||||
|
>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: 'Customer ID',
|
||||||
|
value: (
|
||||||
|
<Chip
|
||||||
|
label={model.id}
|
||||||
|
size="small"
|
||||||
|
variant="soft"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ key: 'Name', value: model.cat_name },
|
||||||
|
{ key: 'Remarks', value: model.remarks },
|
||||||
|
{ key: 'Description', value: model.description },
|
||||||
|
] satisfies { key: string; value: React.ReactNode }[]
|
||||||
|
).map(
|
||||||
|
(item): React.JSX.Element => (
|
||||||
|
<PropertyItem
|
||||||
|
key={item.key}
|
||||||
|
name={item.key}
|
||||||
|
value={item.value}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</PropertyList>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,73 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||||
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||||
|
|
||||||
|
function getImageUrlFrRecord(record: LpCategory): string {
|
||||||
|
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center', flex: '1 1 auto' }}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
variant="rounded"
|
||||||
|
src={getImageUrlFrRecord(lpModel)}
|
||||||
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
|
>
|
||||||
|
{t('empty')}
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
|
||||||
|
>
|
||||||
|
<Typography variant="h4">{lpModel.cat_name}</Typography>
|
||||||
|
<Chip
|
||||||
|
icon={
|
||||||
|
<CheckCircleIcon
|
||||||
|
color="var(--mui-palette-success-main)"
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={lpModel.visible}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Typography
|
||||||
|
color="text.secondary"
|
||||||
|
variant="body1"
|
||||||
|
>
|
||||||
|
{lpModel.slug}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
endIcon={<CaretDownIcon />}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
{t('list.action')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
138
002_source/cms/src/app/dashboard/cr/questions/[cat_id]/page.tsx
Normal file
138
002_source/cms/src/app/dashboard/cr/questions/[cat_id]/page.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import RouterLink from 'next/link';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
|
||||||
|
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
|
||||||
|
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
|
||||||
|
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
|
||||||
|
import { COL_QUIZ_CR_QUESTIONS } from '@/constants';
|
||||||
|
import { Grid } from '@mui/material';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||||
|
import type { RecordModel } from 'pocketbase';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { paths } from '@/paths';
|
||||||
|
import { logger } from '@/lib/default-logger';
|
||||||
|
import { pb } from '@/lib/pb';
|
||||||
|
import { toast } from '@/components/core/toaster';
|
||||||
|
import ErrorDisplay from '@/components/dashboard/error';
|
||||||
|
import { defaultCrQuestion } from '@/components/dashboard/cr/questions/_constants.ts';
|
||||||
|
import { Notifications } from '@/components/dashboard/cr/questions/notifications';
|
||||||
|
import type { CrQuestion } from '@/components/dashboard/cr/questions/type';
|
||||||
|
import FormLoading from '@/components/loading';
|
||||||
|
|
||||||
|
import BasicDetailCard from './BasicDetailCard';
|
||||||
|
import TitleCard from './TitleCard';
|
||||||
|
|
||||||
|
export default function Page(): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
//
|
||||||
|
const { cat_id: catId } = useParams<{ cat_id: string }>();
|
||||||
|
//
|
||||||
|
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||||
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||||
|
|
||||||
|
//
|
||||||
|
const [showLessonQuestion, setShowLessonQuestion] = React.useState<CrQuestion>(defaultCrQuestion);
|
||||||
|
|
||||||
|
function handleEditClick() {
|
||||||
|
router.push(paths.dashboard.cr_questions.edit(showLessonQuestion.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (catId) {
|
||||||
|
pb.collection(COL_QUIZ_CR_QUESTIONS)
|
||||||
|
.getOne(catId)
|
||||||
|
.then((model: RecordModel) => {
|
||||||
|
setShowLessonQuestion({ ...defaultCrQuestion, ...model });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(err);
|
||||||
|
toast(t('list.error'));
|
||||||
|
|
||||||
|
setShowError({ show: true, detail: JSON.stringify(err) });
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setShowLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [catId]);
|
||||||
|
|
||||||
|
if (showLoading) return <FormLoading />;
|
||||||
|
if (showError.show)
|
||||||
|
return (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t('error.unable-to-process-request')}
|
||||||
|
code="500"
|
||||||
|
details={showError.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxWidth: 'var(--Content-maxWidth)',
|
||||||
|
m: 'var(--Content-margin)',
|
||||||
|
p: 'var(--Content-padding)',
|
||||||
|
width: 'var(--Content-width)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
color="text.primary"
|
||||||
|
component={RouterLink}
|
||||||
|
href={paths.dashboard.cr_questions.list}
|
||||||
|
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||||
|
variant="subtitle2"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||||
|
{t('edit.title')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
|
spacing={3}
|
||||||
|
sx={{ alignItems: 'flex-start' }}
|
||||||
|
>
|
||||||
|
<TitleCard lpModel={showLessonQuestion} />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={4}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
lg={4}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<BasicDetailCard
|
||||||
|
lpModel={showLessonQuestion}
|
||||||
|
handleEditClick={handleEditClick}
|
||||||
|
/>
|
||||||
|
<SampleSecurityCard />
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
lg={8}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<SamplePaymentCard />
|
||||||
|
<SampleAddressCard />
|
||||||
|
<Notifications notifications={SampleNotifications} />
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,90 @@
|
|||||||
|
import { dayjs } from '@/lib/dayjs';
|
||||||
|
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||||
|
|
||||||
|
export const CrCategoriesSampleData = [
|
||||||
|
{
|
||||||
|
id: 'USR-005',
|
||||||
|
name: 'Fran Perez',
|
||||||
|
avatar: '/assets/avatar-5.png',
|
||||||
|
email: 'fran.perez@domain.com',
|
||||||
|
phone: '(815) 704-0045',
|
||||||
|
quota: 50,
|
||||||
|
status: 'active',
|
||||||
|
createdAt: dayjs().subtract(1, 'hour').toDate(),
|
||||||
|
collectionId: '0000000001',
|
||||||
|
cat_name: '',
|
||||||
|
pos: 99,
|
||||||
|
visible: 'visible',
|
||||||
|
lesson_id: 'lid_00001',
|
||||||
|
description: '',
|
||||||
|
remarks: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'USR-004',
|
||||||
|
name: 'Penjani Inyene',
|
||||||
|
avatar: '/assets/avatar-4.png',
|
||||||
|
email: 'penjani.inyene@domain.com',
|
||||||
|
phone: '(803) 937-8925',
|
||||||
|
quota: 100,
|
||||||
|
status: 'active',
|
||||||
|
createdAt: dayjs().subtract(3, 'hour').toDate(),
|
||||||
|
collectionId: '0000000001',
|
||||||
|
cat_name: '',
|
||||||
|
pos: 99,
|
||||||
|
visible: 'visible',
|
||||||
|
lesson_id: 'lid_00001',
|
||||||
|
description: '',
|
||||||
|
remarks: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'USR-003',
|
||||||
|
name: 'Carson Darrin',
|
||||||
|
avatar: '/assets/avatar-3.png',
|
||||||
|
email: 'carson.darrin@domain.com',
|
||||||
|
phone: '(715) 278-5041',
|
||||||
|
quota: 10,
|
||||||
|
status: 'blocked',
|
||||||
|
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
|
||||||
|
collectionId: '0000000001',
|
||||||
|
cat_name: '',
|
||||||
|
pos: 99,
|
||||||
|
visible: 'visible',
|
||||||
|
lesson_id: 'lid_00001',
|
||||||
|
description: '',
|
||||||
|
remarks: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'USR-002',
|
||||||
|
name: 'Siegbert Gottfried',
|
||||||
|
avatar: '/assets/avatar-2.png',
|
||||||
|
email: 'siegbert.gottfried@domain.com',
|
||||||
|
phone: '(603) 766-0431',
|
||||||
|
quota: 0,
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
|
||||||
|
collectionId: '0000000001',
|
||||||
|
cat_name: '',
|
||||||
|
pos: 99,
|
||||||
|
visible: 'visible',
|
||||||
|
lesson_id: 'lid_00001',
|
||||||
|
description: '',
|
||||||
|
remarks: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'USR-001',
|
||||||
|
name: 'Miron Vitold',
|
||||||
|
avatar: '/assets/avatar-1.png',
|
||||||
|
email: 'miron.vitold@domain.com',
|
||||||
|
phone: '(425) 434-5535',
|
||||||
|
quota: 50,
|
||||||
|
status: 'active',
|
||||||
|
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
||||||
|
collectionId: '0000000001',
|
||||||
|
cat_name: '',
|
||||||
|
pos: 99,
|
||||||
|
visible: 'visible',
|
||||||
|
lesson_id: 'lid_00001',
|
||||||
|
description: '',
|
||||||
|
remarks: '',
|
||||||
|
},
|
||||||
|
] satisfies LessonCategory[];
|
@@ -1,19 +1,24 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
// RULES:
|
||||||
|
// T.B.A.
|
||||||
|
//
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { Metadata } from 'next';
|
|
||||||
import RouterLink from 'next/link';
|
import RouterLink from 'next/link';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { config } from '@/config';
|
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
import { CustomerCreateForm } from '@/components/dashboard/customer/customer-create-form';
|
import { CrQuestionCreateForm } from '@/components/dashboard/cr/questions/cr-question-create-form';
|
||||||
|
|
||||||
export const metadata = { title: `Create | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
|
||||||
|
|
||||||
export default function Page(): React.JSX.Element {
|
export default function Page(): React.JSX.Element {
|
||||||
|
// RULES: follow the name of page directory
|
||||||
|
const { t } = useTranslation(['lp_questions']);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -29,19 +34,19 @@ export default function Page(): React.JSX.Element {
|
|||||||
<Link
|
<Link
|
||||||
color="text.primary"
|
color="text.primary"
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
href={paths.dashboard.customers.list}
|
href={paths.dashboard.lp_questions.list}
|
||||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||||
variant="subtitle2"
|
variant="subtitle2"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||||
Customers
|
{t('title')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="h4">Create customer</Typography>
|
<Typography variant="h4">{t('create.title')}</Typography>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
<CustomerCreateForm />
|
<CrQuestionCreateForm />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
@@ -0,0 +1,11 @@
|
|||||||
|
# task
|
||||||
|
|
||||||
|
## instruction
|
||||||
|
|
||||||
|
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/_helloworld/page.tsx`
|
||||||
|
|
||||||
|
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_types/edit/[typeId]/page.tsx`
|
||||||
|
|
||||||
|
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_categories/edit/page.tsx`
|
||||||
|
|
||||||
|
please draft a tsx for showing error to user thanks,
|
@@ -1,19 +1,24 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { Metadata } from 'next';
|
|
||||||
import RouterLink from 'next/link';
|
import RouterLink from 'next/link';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { config } from '@/config';
|
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
import { CustomerCreateForm } from '@/components/dashboard/customer/customer-create-form';
|
import { CrQuestionEditForm } from '@/components/dashboard/cr/questions/cr-question-edit-form';
|
||||||
|
|
||||||
export const metadata = { title: `Create | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
|
||||||
|
|
||||||
export default function Page(): React.JSX.Element {
|
export default function Page(): React.JSX.Element {
|
||||||
|
const { t } = useTranslation(['lp_questions']);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// console.log('helloworld');
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -29,19 +34,19 @@ export default function Page(): React.JSX.Element {
|
|||||||
<Link
|
<Link
|
||||||
color="text.primary"
|
color="text.primary"
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
href={paths.dashboard.customers.list}
|
href={paths.dashboard.lp_questions.list}
|
||||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||||
variant="subtitle2"
|
variant="subtitle2"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||||
Customers
|
{t('edit.title')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="h4">Create customer</Typography>
|
<Typography variant="h4">{t('edit.title')}</Typography>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
<CustomerCreateForm />
|
<CrQuestionEditForm />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
272
002_source/cms/src/app/dashboard/cr/questions/page.tsx
Normal file
272
002_source/cms/src/app/dashboard/cr/questions/page.tsx
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
// RULES:
|
||||||
|
// contains list page for cr_questions (QuizCRQuestions)
|
||||||
|
// contain definition to collection only
|
||||||
|
//
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { COL_QUIZ_CR_QUESTIONS } from '@/constants';
|
||||||
|
import { LoadingButton } from '@mui/lab';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||||
|
import type { ListResult, RecordModel } from 'pocketbase';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { paths } from '@/paths';
|
||||||
|
import isDevelopment from '@/lib/check-is-development';
|
||||||
|
import { logger } from '@/lib/default-logger';
|
||||||
|
import { pb } from '@/lib/pb';
|
||||||
|
import { toast } from '@/components/core/toaster';
|
||||||
|
import ErrorDisplay from '@/components/dashboard/error';
|
||||||
|
import { defaultCrQuestion } from '@/components/dashboard/cr/questions/_constants';
|
||||||
|
import { CrQuestionsFilters } from '@/components/dashboard/cr/questions/cr-questions-filters';
|
||||||
|
import type { Filters } from '@/components/dashboard/cr/questions/cr-questions-filters';
|
||||||
|
import { CrQuestionsPagination } from '@/components/dashboard/cr/questions/cr-questions-pagination';
|
||||||
|
import { CrQuestionsSelectionProvider } from '@/components/dashboard/cr/questions/cr-questions-selection-context';
|
||||||
|
import { CrQuestionsTable } from '@/components/dashboard/cr/questions/cr-questions-table';
|
||||||
|
import type { CrQuestion } from '@/components/dashboard/cr/questions/type';
|
||||||
|
import FormLoading from '@/components/loading';
|
||||||
|
|
||||||
|
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||||
|
const { t } = useTranslation(['lp_questions']);
|
||||||
|
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||||
|
const router = useRouter();
|
||||||
|
const [lessonQuestionsData, setLessonCategoriesData] = React.useState<CrQuestion[]>([]);
|
||||||
|
//
|
||||||
|
|
||||||
|
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
||||||
|
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||||
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||||
|
//
|
||||||
|
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||||
|
//
|
||||||
|
const [f, setF] = React.useState<CrQuestion[]>([]);
|
||||||
|
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||||
|
//
|
||||||
|
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||||
|
const [listOption, setListOption] = React.useState({});
|
||||||
|
const [listSort, setListSort] = React.useState({});
|
||||||
|
|
||||||
|
//
|
||||||
|
const sortedLessonCategories = applySort(lessonQuestionsData, sortDir);
|
||||||
|
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status });
|
||||||
|
|
||||||
|
const reloadRows = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const models: ListResult<RecordModel> = await pb
|
||||||
|
.collection(COL_QUIZ_CR_QUESTIONS)
|
||||||
|
.getList(currentPage + 1, rowsPerPage, listOption);
|
||||||
|
const { items, totalItems } = models;
|
||||||
|
const tempLessonTypes: CrQuestion[] = items.map((lt) => {
|
||||||
|
return { ...defaultCrQuestion, ...lt };
|
||||||
|
});
|
||||||
|
|
||||||
|
setLessonCategoriesData(tempLessonTypes);
|
||||||
|
setRecordCount(totalItems);
|
||||||
|
setF(tempLessonTypes);
|
||||||
|
// console.log({ currentPage, f });
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
logger.error(error);
|
||||||
|
setShowError({
|
||||||
|
//
|
||||||
|
show: true,
|
||||||
|
detail: JSON.stringify(error, null, 2),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setShowLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [lastListOption, setLastListOption] = React.useState({});
|
||||||
|
const isFirstRun = React.useRef(false);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isFirstRun.current) {
|
||||||
|
isFirstRun.current = true;
|
||||||
|
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
||||||
|
// reset page number as tab changes
|
||||||
|
setLastListOption(listOption);
|
||||||
|
setCurrentPage(0);
|
||||||
|
void reloadRows();
|
||||||
|
} else {
|
||||||
|
void reloadRows();
|
||||||
|
}
|
||||||
|
}, [currentPage, rowsPerPage, listOption]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let tempFilter = [],
|
||||||
|
tempSortDir = '';
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
tempFilter.push(`visible = "${visible}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDir) {
|
||||||
|
tempSortDir = `-created`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
tempFilter.push(`name ~ "%${name}%"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
tempFilter.push(`type ~ "%${type}%"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let preFinalListOption = {};
|
||||||
|
if (tempFilter.length > 0) {
|
||||||
|
preFinalListOption = { filter: tempFilter.join(' && ') };
|
||||||
|
}
|
||||||
|
if (tempSortDir.length > 0) {
|
||||||
|
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
|
||||||
|
}
|
||||||
|
setListOption(preFinalListOption);
|
||||||
|
// setListOption({
|
||||||
|
// filter: tempFilter.join(' && '),
|
||||||
|
// sort: tempSortDir,
|
||||||
|
// //
|
||||||
|
// });
|
||||||
|
}, [visible, sortDir, name, type]);
|
||||||
|
|
||||||
|
// return <>helloworld</>;
|
||||||
|
|
||||||
|
if (showLoading) return <FormLoading />;
|
||||||
|
|
||||||
|
if (showError.show)
|
||||||
|
return (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t('error.unable-to-process-request')}
|
||||||
|
code={-1}
|
||||||
|
details={showError.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxWidth: 'var(--Content-maxWidth)',
|
||||||
|
m: 'var(--Content-margin)',
|
||||||
|
p: 'var(--Content-padding)',
|
||||||
|
width: 'var(--Content-width)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
|
spacing={3}
|
||||||
|
sx={{ alignItems: 'flex-start' }}
|
||||||
|
>
|
||||||
|
<Box sx={{ flex: '1 1 auto' }}>
|
||||||
|
<Typography variant="h4">{t('list.title')}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<LoadingButton
|
||||||
|
loading={isLoadingAddPage}
|
||||||
|
onClick={(): void => {
|
||||||
|
setIsLoadingAddPage(true);
|
||||||
|
router.push(paths.dashboard.cr_questions.create);
|
||||||
|
}}
|
||||||
|
startIcon={<PlusIcon />}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
{t('list.add')}
|
||||||
|
</LoadingButton>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<CrQuestionsSelectionProvider lessonQuestions={f}>
|
||||||
|
<Card>
|
||||||
|
<CrQuestionsFilters
|
||||||
|
filters={{ email, phone, status, name, visible, type }}
|
||||||
|
fullData={lessonQuestionsData}
|
||||||
|
sortDir={sortDir}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<Box sx={{ overflowX: 'auto' }}>
|
||||||
|
<CrQuestionsTable
|
||||||
|
reloadRows={reloadRows}
|
||||||
|
rows={f}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<CrQuestionsPagination
|
||||||
|
count={recordCount}
|
||||||
|
page={currentPage}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
setPage={setCurrentPage}
|
||||||
|
setRowsPerPage={setRowsPerPage}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</CrQuestionsSelectionProvider>
|
||||||
|
</Stack>
|
||||||
|
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
|
||||||
|
<pre>{JSON.stringify(f, null, 2)}</pre>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting and filtering has to be done on the server.
|
||||||
|
|
||||||
|
function applySort(row: CrQuestion[], sortDir: 'asc' | 'desc' | undefined): CrQuestion[] {
|
||||||
|
return row.sort((a, b) => {
|
||||||
|
if (sortDir === 'asc') {
|
||||||
|
return a.createdAt.getTime() - b.createdAt.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.createdAt.getTime() - a.createdAt.getTime();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilters(row: CrQuestion[], { email, phone, status, name, visible }: Filters): CrQuestion[] {
|
||||||
|
return row.filter((item) => {
|
||||||
|
if (email) {
|
||||||
|
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phone) {
|
||||||
|
if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
if (item.status !== status) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
if (!item.name?.toLowerCase().includes(name.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
searchParams: {
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
sortDir?: 'asc' | 'desc';
|
||||||
|
status?: string;
|
||||||
|
name?: string;
|
||||||
|
visible?: string;
|
||||||
|
type?: string;
|
||||||
|
//
|
||||||
|
};
|
||||||
|
}
|
1772
002_source/cms/src/app/dashboard/cr/repomix-output.xml
Normal file
1772
002_source/cms/src/app/dashboard/cr/repomix-output.xml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,308 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import type { Metadata } from 'next';
|
|
||||||
import RouterLink from 'next/link';
|
|
||||||
import Avatar from '@mui/material/Avatar';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import Card from '@mui/material/Card';
|
|
||||||
import CardContent from '@mui/material/CardContent';
|
|
||||||
import CardHeader from '@mui/material/CardHeader';
|
|
||||||
import Chip from '@mui/material/Chip';
|
|
||||||
import Divider from '@mui/material/Divider';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
|
||||||
import LinearProgress from '@mui/material/LinearProgress';
|
|
||||||
import Link from '@mui/material/Link';
|
|
||||||
import Stack from '@mui/material/Stack';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import Grid from '@mui/material/Unstable_Grid2';
|
|
||||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
|
||||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
|
||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
|
||||||
import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard';
|
|
||||||
import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House';
|
|
||||||
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
|
||||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
|
||||||
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
|
||||||
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
|
||||||
|
|
||||||
import { config } from '@/config';
|
|
||||||
import { paths } from '@/paths';
|
|
||||||
import { dayjs } from '@/lib/dayjs';
|
|
||||||
import { PropertyItem } from '@/components/core/property-item';
|
|
||||||
import { PropertyList } from '@/components/core/property-list';
|
|
||||||
import { Notifications } from '@/components/dashboard/customer/notifications';
|
|
||||||
import { Payments } from '@/components/dashboard/customer/payments';
|
|
||||||
import type { Address } from '@/components/dashboard/customer/shipping-address';
|
|
||||||
import { ShippingAddress } from '@/components/dashboard/customer/shipping-address';
|
|
||||||
|
|
||||||
export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
|
||||||
|
|
||||||
export default function Page(): React.JSX.Element {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
maxWidth: 'var(--Content-maxWidth)',
|
|
||||||
m: 'var(--Content-margin)',
|
|
||||||
p: 'var(--Content-padding)',
|
|
||||||
width: 'var(--Content-width)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack spacing={4}>
|
|
||||||
<Stack spacing={3}>
|
|
||||||
<div>
|
|
||||||
<Link
|
|
||||||
color="text.primary"
|
|
||||||
component={RouterLink}
|
|
||||||
href={paths.dashboard.customers.list}
|
|
||||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
|
||||||
variant="subtitle2"
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
|
||||||
Customers
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
|
|
||||||
<Avatar src="/assets/avatar-1.png" sx={{ '--Avatar-size': '64px' }}>
|
|
||||||
MV
|
|
||||||
</Avatar>
|
|
||||||
<div>
|
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
|
|
||||||
<Typography variant="h4">Miron Vitold</Typography>
|
|
||||||
<Chip
|
|
||||||
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
|
|
||||||
label="Active"
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Typography color="text.secondary" variant="body1">
|
|
||||||
miron.vitold@domain.com
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
<div>
|
|
||||||
<Button endIcon={<CaretDownIcon />} variant="contained">
|
|
||||||
Action
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
<Grid container spacing={4}>
|
|
||||||
<Grid lg={4} xs={12}>
|
|
||||||
<Stack spacing={4}>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
action={
|
|
||||||
<IconButton>
|
|
||||||
<PencilSimpleIcon />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<UserIcon fontSize="var(--Icon-fontSize)" />
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
title="Basic details"
|
|
||||||
/>
|
|
||||||
<PropertyList
|
|
||||||
divider={<Divider />}
|
|
||||||
orientation="vertical"
|
|
||||||
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
|
||||||
>
|
|
||||||
{(
|
|
||||||
[
|
|
||||||
{ key: 'Customer ID', value: <Chip label="USR-001" size="small" variant="soft" /> },
|
|
||||||
{ key: 'Name', value: 'Miron Vitold' },
|
|
||||||
{ key: 'Email', value: 'miron.vitold@domain.com' },
|
|
||||||
{ key: 'Phone', value: '(425) 434-5535' },
|
|
||||||
{ key: 'Company', value: 'Devias IO' },
|
|
||||||
{
|
|
||||||
key: 'Quota',
|
|
||||||
value: (
|
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
|
||||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" />
|
|
||||||
<Typography color="text.secondary" variant="body2">
|
|
||||||
50%
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
] satisfies { key: string; value: React.ReactNode }[]
|
|
||||||
).map(
|
|
||||||
(item): React.JSX.Element => (
|
|
||||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</PropertyList>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
title="Security"
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
<Stack spacing={1}>
|
|
||||||
<div>
|
|
||||||
<Button color="error" variant="contained">
|
|
||||||
Delete account
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Typography color="text.secondary" variant="body2">
|
|
||||||
A deleted customer cannot be restored. All data will be permanently removed.
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
<Grid lg={8} xs={12}>
|
|
||||||
<Stack spacing={4}>
|
|
||||||
<Payments
|
|
||||||
ordersValue={2069.48}
|
|
||||||
payments={[
|
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 500,
|
|
||||||
invoiceId: 'INV-005',
|
|
||||||
status: 'completed',
|
|
||||||
createdAt: dayjs().subtract(5, 'minute').subtract(1, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 324.5,
|
|
||||||
invoiceId: 'INV-004',
|
|
||||||
status: 'refunded',
|
|
||||||
createdAt: dayjs().subtract(21, 'minute').subtract(2, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 746.5,
|
|
||||||
invoiceId: 'INV-003',
|
|
||||||
status: 'completed',
|
|
||||||
createdAt: dayjs().subtract(7, 'minute').subtract(3, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 56.89,
|
|
||||||
invoiceId: 'INV-002',
|
|
||||||
status: 'completed',
|
|
||||||
createdAt: dayjs().subtract(48, 'minute').subtract(4, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 541.59,
|
|
||||||
invoiceId: 'INV-001',
|
|
||||||
status: 'completed',
|
|
||||||
createdAt: dayjs().subtract(31, 'minute').subtract(5, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
refundsValue={324.5}
|
|
||||||
totalOrders={5}
|
|
||||||
/>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
action={
|
|
||||||
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
title="Billing details"
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
<Card sx={{ borderRadius: 1 }} variant="outlined">
|
|
||||||
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}>
|
|
||||||
{(
|
|
||||||
[
|
|
||||||
{ key: 'Credit card', value: '**** 4142' },
|
|
||||||
{ key: 'Country', value: 'United States' },
|
|
||||||
{ key: 'State', value: 'Michigan' },
|
|
||||||
{ key: 'City', value: 'Southfield' },
|
|
||||||
{ key: 'Address', value: '1721 Bartlett Avenue, 48034' },
|
|
||||||
{ key: 'Tax ID', value: 'EU87956621' },
|
|
||||||
] satisfies { key: string; value: React.ReactNode }[]
|
|
||||||
).map(
|
|
||||||
(item): React.JSX.Element => (
|
|
||||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</PropertyList>
|
|
||||||
</Card>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
action={
|
|
||||||
<Button color="secondary" startIcon={<PlusIcon />}>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<HouseIcon fontSize="var(--Icon-fontSize)" />
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
title="Shipping addresses"
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
<Grid container spacing={3}>
|
|
||||||
{(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: 'ADR-001',
|
|
||||||
country: 'United States',
|
|
||||||
state: 'Michigan',
|
|
||||||
city: 'Lansing',
|
|
||||||
zipCode: '48933',
|
|
||||||
street: '480 Haven Lane',
|
|
||||||
primary: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ADR-002',
|
|
||||||
country: 'United States',
|
|
||||||
state: 'Missouri',
|
|
||||||
city: 'Springfield',
|
|
||||||
zipCode: '65804',
|
|
||||||
street: '4807 Lighthouse Drive',
|
|
||||||
},
|
|
||||||
] satisfies Address[]
|
|
||||||
).map((address) => (
|
|
||||||
<Grid key={address.id} md={6} xs={12}>
|
|
||||||
<ShippingAddress address={address} />
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Notifications
|
|
||||||
notifications={[
|
|
||||||
{
|
|
||||||
id: 'EV-002',
|
|
||||||
type: 'Refund request approved',
|
|
||||||
status: 'pending',
|
|
||||||
createdAt: dayjs().subtract(34, 'minute').subtract(5, 'hour').subtract(3, 'day').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'EV-001',
|
|
||||||
type: 'Order confirmation',
|
|
||||||
status: 'delivered',
|
|
||||||
createdAt: dayjs().subtract(49, 'minute').subtract(11, 'hour').subtract(4, 'day').toDate(),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,308 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import type { Metadata } from 'next';
|
|
||||||
import RouterLink from 'next/link';
|
|
||||||
import Avatar from '@mui/material/Avatar';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import Card from '@mui/material/Card';
|
|
||||||
import CardContent from '@mui/material/CardContent';
|
|
||||||
import CardHeader from '@mui/material/CardHeader';
|
|
||||||
import Chip from '@mui/material/Chip';
|
|
||||||
import Divider from '@mui/material/Divider';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
|
||||||
import LinearProgress from '@mui/material/LinearProgress';
|
|
||||||
import Link from '@mui/material/Link';
|
|
||||||
import Stack from '@mui/material/Stack';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import Grid from '@mui/material/Unstable_Grid2';
|
|
||||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
|
||||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
|
||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
|
||||||
import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard';
|
|
||||||
import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House';
|
|
||||||
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
|
||||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
|
||||||
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
|
||||||
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
|
||||||
|
|
||||||
import { config } from '@/config';
|
|
||||||
import { paths } from '@/paths';
|
|
||||||
import { dayjs } from '@/lib/dayjs';
|
|
||||||
import { PropertyItem } from '@/components/core/property-item';
|
|
||||||
import { PropertyList } from '@/components/core/property-list';
|
|
||||||
import { Notifications } from '@/components/dashboard/customer/notifications';
|
|
||||||
import { Payments } from '@/components/dashboard/customer/payments';
|
|
||||||
import type { Address } from '@/components/dashboard/customer/shipping-address';
|
|
||||||
import { ShippingAddress } from '@/components/dashboard/customer/shipping-address';
|
|
||||||
|
|
||||||
export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
|
||||||
|
|
||||||
export default function Page(): React.JSX.Element {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
maxWidth: 'var(--Content-maxWidth)',
|
|
||||||
m: 'var(--Content-margin)',
|
|
||||||
p: 'var(--Content-padding)',
|
|
||||||
width: 'var(--Content-width)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack spacing={4}>
|
|
||||||
<Stack spacing={3}>
|
|
||||||
<div>
|
|
||||||
<Link
|
|
||||||
color="text.primary"
|
|
||||||
component={RouterLink}
|
|
||||||
href={paths.dashboard.customers.list}
|
|
||||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
|
||||||
variant="subtitle2"
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
|
||||||
Customers
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
|
|
||||||
<Avatar src="/assets/avatar-1.png" sx={{ '--Avatar-size': '64px' }}>
|
|
||||||
MV
|
|
||||||
</Avatar>
|
|
||||||
<div>
|
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
|
|
||||||
<Typography variant="h4">Miron Vitold</Typography>
|
|
||||||
<Chip
|
|
||||||
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
|
|
||||||
label="Active"
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Typography color="text.secondary" variant="body1">
|
|
||||||
miron.vitold@domain.com
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
<div>
|
|
||||||
<Button endIcon={<CaretDownIcon />} variant="contained">
|
|
||||||
Action
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
<Grid container spacing={4}>
|
|
||||||
<Grid lg={4} xs={12}>
|
|
||||||
<Stack spacing={4}>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
action={
|
|
||||||
<IconButton>
|
|
||||||
<PencilSimpleIcon />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<UserIcon fontSize="var(--Icon-fontSize)" />
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
title="Basic details"
|
|
||||||
/>
|
|
||||||
<PropertyList
|
|
||||||
divider={<Divider />}
|
|
||||||
orientation="vertical"
|
|
||||||
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
|
||||||
>
|
|
||||||
{(
|
|
||||||
[
|
|
||||||
{ key: 'Customer ID', value: <Chip label="USR-001" size="small" variant="soft" /> },
|
|
||||||
{ key: 'Name', value: 'Miron Vitold' },
|
|
||||||
{ key: 'Email', value: 'miron.vitold@domain.com' },
|
|
||||||
{ key: 'Phone', value: '(425) 434-5535' },
|
|
||||||
{ key: 'Company', value: 'Devias IO' },
|
|
||||||
{
|
|
||||||
key: 'Quota',
|
|
||||||
value: (
|
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
|
||||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" />
|
|
||||||
<Typography color="text.secondary" variant="body2">
|
|
||||||
50%
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
] satisfies { key: string; value: React.ReactNode }[]
|
|
||||||
).map(
|
|
||||||
(item): React.JSX.Element => (
|
|
||||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</PropertyList>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
title="Security"
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
<Stack spacing={1}>
|
|
||||||
<div>
|
|
||||||
<Button color="error" variant="contained">
|
|
||||||
Delete account
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Typography color="text.secondary" variant="body2">
|
|
||||||
A deleted customer cannot be restored. All data will be permanently removed.
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
<Grid lg={8} xs={12}>
|
|
||||||
<Stack spacing={4}>
|
|
||||||
<Payments
|
|
||||||
ordersValue={2069.48}
|
|
||||||
payments={[
|
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 500,
|
|
||||||
invoiceId: 'INV-005',
|
|
||||||
status: 'completed',
|
|
||||||
createdAt: dayjs().subtract(5, 'minute').subtract(1, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 324.5,
|
|
||||||
invoiceId: 'INV-004',
|
|
||||||
status: 'refunded',
|
|
||||||
createdAt: dayjs().subtract(21, 'minute').subtract(2, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 746.5,
|
|
||||||
invoiceId: 'INV-003',
|
|
||||||
status: 'completed',
|
|
||||||
createdAt: dayjs().subtract(7, 'minute').subtract(3, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 56.89,
|
|
||||||
invoiceId: 'INV-002',
|
|
||||||
status: 'completed',
|
|
||||||
createdAt: dayjs().subtract(48, 'minute').subtract(4, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 541.59,
|
|
||||||
invoiceId: 'INV-001',
|
|
||||||
status: 'completed',
|
|
||||||
createdAt: dayjs().subtract(31, 'minute').subtract(5, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
refundsValue={324.5}
|
|
||||||
totalOrders={5}
|
|
||||||
/>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
action={
|
|
||||||
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
title="Billing details"
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
<Card sx={{ borderRadius: 1 }} variant="outlined">
|
|
||||||
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}>
|
|
||||||
{(
|
|
||||||
[
|
|
||||||
{ key: 'Credit card', value: '**** 4142' },
|
|
||||||
{ key: 'Country', value: 'United States' },
|
|
||||||
{ key: 'State', value: 'Michigan' },
|
|
||||||
{ key: 'City', value: 'Southfield' },
|
|
||||||
{ key: 'Address', value: '1721 Bartlett Avenue, 48034' },
|
|
||||||
{ key: 'Tax ID', value: 'EU87956621' },
|
|
||||||
] satisfies { key: string; value: React.ReactNode }[]
|
|
||||||
).map(
|
|
||||||
(item): React.JSX.Element => (
|
|
||||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</PropertyList>
|
|
||||||
</Card>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
action={
|
|
||||||
<Button color="secondary" startIcon={<PlusIcon />}>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<HouseIcon fontSize="var(--Icon-fontSize)" />
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
title="Shipping addresses"
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
<Grid container spacing={3}>
|
|
||||||
{(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: 'ADR-001',
|
|
||||||
country: 'United States',
|
|
||||||
state: 'Michigan',
|
|
||||||
city: 'Lansing',
|
|
||||||
zipCode: '48933',
|
|
||||||
street: '480 Haven Lane',
|
|
||||||
primary: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ADR-002',
|
|
||||||
country: 'United States',
|
|
||||||
state: 'Missouri',
|
|
||||||
city: 'Springfield',
|
|
||||||
zipCode: '65804',
|
|
||||||
street: '4807 Lighthouse Drive',
|
|
||||||
},
|
|
||||||
] satisfies Address[]
|
|
||||||
).map((address) => (
|
|
||||||
<Grid key={address.id} md={6} xs={12}>
|
|
||||||
<ShippingAddress address={address} />
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Notifications
|
|
||||||
notifications={[
|
|
||||||
{
|
|
||||||
id: 'EV-002',
|
|
||||||
type: 'Refund request approved',
|
|
||||||
status: 'pending',
|
|
||||||
createdAt: dayjs().subtract(34, 'minute').subtract(5, 'hour').subtract(3, 'day').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'EV-001',
|
|
||||||
type: 'Order confirmation',
|
|
||||||
status: 'delivered',
|
|
||||||
createdAt: dayjs().subtract(49, 'minute').subtract(11, 'hour').subtract(4, 'day').toDate(),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,25 +1,9 @@
|
|||||||
import * as React from 'react';
|
// src/app/dashboard/customers/page.tsx
|
||||||
import type { Metadata } from 'next';
|
'use client';
|
||||||
import Box from '@mui/material/Box';
|
import type { Customer } from '@/components/dashboard/customer/type.d';
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import Card from '@mui/material/Card';
|
|
||||||
import Divider from '@mui/material/Divider';
|
|
||||||
import Stack from '@mui/material/Stack';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
|
||||||
|
|
||||||
import { config } from '@/config';
|
|
||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { CustomersFilters } from '@/components/dashboard/customer/customers-filters';
|
|
||||||
import type { Filters } from '@/components/dashboard/customer/customers-filters';
|
|
||||||
import { CustomersPagination } from '@/components/dashboard/customer/customers-pagination';
|
|
||||||
import { CustomersSelectionProvider } from '@/components/dashboard/customer/customers-selection-context';
|
|
||||||
import { CustomersTable } from '@/components/dashboard/customer/customers-table';
|
|
||||||
import type { Customer } from '@/components/dashboard/customer/customers-table';
|
|
||||||
|
|
||||||
export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
export const SampleCustomers = [
|
||||||
|
|
||||||
const customers = [
|
|
||||||
{
|
{
|
||||||
id: 'USR-005',
|
id: 'USR-005',
|
||||||
name: 'Fran Perez',
|
name: 'Fran Perez',
|
||||||
@@ -171,85 +155,3 @@ const customers = [
|
|||||||
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
||||||
},
|
},
|
||||||
] satisfies Customer[];
|
] satisfies Customer[];
|
||||||
|
|
||||||
interface PageProps {
|
|
||||||
searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|
||||||
const { email, phone, sortDir, status } = searchParams;
|
|
||||||
|
|
||||||
const sortedCustomers = applySort(customers, sortDir);
|
|
||||||
const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
maxWidth: 'var(--Content-maxWidth)',
|
|
||||||
m: 'var(--Content-margin)',
|
|
||||||
p: 'var(--Content-padding)',
|
|
||||||
width: 'var(--Content-width)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack spacing={4}>
|
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
|
||||||
<Box sx={{ flex: '1 1 auto' }}>
|
|
||||||
<Typography variant="h4">Customers</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
||||||
<Button startIcon={<PlusIcon />} variant="contained">
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
<CustomersSelectionProvider customers={filteredCustomers}>
|
|
||||||
<Card>
|
|
||||||
<CustomersFilters filters={{ email, phone, status }} sortDir={sortDir} />
|
|
||||||
<Divider />
|
|
||||||
<Box sx={{ overflowX: 'auto' }}>
|
|
||||||
<CustomersTable rows={filteredCustomers} />
|
|
||||||
</Box>
|
|
||||||
<Divider />
|
|
||||||
<CustomersPagination count={filteredCustomers.length + 100} page={0} />
|
|
||||||
</Card>
|
|
||||||
</CustomersSelectionProvider>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sorting and filtering has to be done on the server.
|
|
||||||
|
|
||||||
function applySort(row: Customer[], sortDir: 'asc' | 'desc' | undefined): Customer[] {
|
|
||||||
return row.sort((a, b) => {
|
|
||||||
if (sortDir === 'asc') {
|
|
||||||
return a.createdAt.getTime() - b.createdAt.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.createdAt.getTime() - a.createdAt.getTime();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyFilters(row: Customer[], { email, phone, status }: Filters): Customer[] {
|
|
||||||
return row.filter((item) => {
|
|
||||||
if (email) {
|
|
||||||
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (phone) {
|
|
||||||
if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
if (item.status !== status) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
@@ -0,0 +1,80 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardHeader from '@mui/material/CardHeader';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||||
|
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { PropertyItem } from '@/components/core/property-item';
|
||||||
|
import { PropertyList } from '@/components/core/property-list';
|
||||||
|
// import { CrCategory } from '@/components/dashboard/cr/categories/type';
|
||||||
|
import type { Customer } from '@/components/dashboard/customer/type.d';
|
||||||
|
|
||||||
|
export default function BasicDetailCard({
|
||||||
|
lpModel: model,
|
||||||
|
handleEditClick,
|
||||||
|
}: {
|
||||||
|
lpModel: Customer;
|
||||||
|
handleEditClick: () => void;
|
||||||
|
}): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
action={
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
handleEditClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PencilSimpleIcon />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
avatar={
|
||||||
|
<Avatar>
|
||||||
|
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
title={t('list.basic-details')}
|
||||||
|
/>
|
||||||
|
<PropertyList
|
||||||
|
divider={<Divider />}
|
||||||
|
orientation="vertical"
|
||||||
|
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
||||||
|
>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: 'Customer ID',
|
||||||
|
value: (
|
||||||
|
<Chip
|
||||||
|
label={model.id}
|
||||||
|
size="small"
|
||||||
|
variant="soft"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ key: 'Email', value: model.email },
|
||||||
|
{ key: 'Quota', value: model.quota },
|
||||||
|
{ key: 'Status', value: model.status },
|
||||||
|
] satisfies { key: string; value: React.ReactNode }[]
|
||||||
|
).map(
|
||||||
|
(item): React.JSX.Element => (
|
||||||
|
<PropertyItem
|
||||||
|
key={item.key}
|
||||||
|
name={item.key}
|
||||||
|
value={item.value}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</PropertyList>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,76 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||||
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { Customer } from '@/components/dashboard/customer/type.d';
|
||||||
|
|
||||||
|
// import type { CrCategory } from '@/components/dashboard/cr/categories/type';
|
||||||
|
|
||||||
|
function getImageUrlFrRecord(record: Customer): string {
|
||||||
|
// TODO: fix this
|
||||||
|
// `http://127.0.0.1:8090/api/files/${'record.collectionId'}/${'record.id'}/${'record.cat_image'}`;
|
||||||
|
return 'getImageUrlFrRecord(helloworld)';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SampleTitleCard({ lpModel }: { lpModel: Customer }): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center', flex: '1 1 auto' }}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
variant="rounded"
|
||||||
|
src={getImageUrlFrRecord(lpModel)}
|
||||||
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
|
>
|
||||||
|
{t('empty')}
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
|
||||||
|
>
|
||||||
|
<Typography variant="h4">{lpModel.email}</Typography>
|
||||||
|
<Chip
|
||||||
|
icon={
|
||||||
|
<CheckCircleIcon
|
||||||
|
color="var(--mui-palette-success-main)"
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={lpModel.quota}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Typography
|
||||||
|
color="text.secondary"
|
||||||
|
variant="body1"
|
||||||
|
>
|
||||||
|
{lpModel.status}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
endIcon={<CaretDownIcon />}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
{t('list.action')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,43 +1,83 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { Metadata } from 'next';
|
|
||||||
import RouterLink from 'next/link';
|
import RouterLink from 'next/link';
|
||||||
import Avatar from '@mui/material/Avatar';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
|
||||||
|
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
|
||||||
|
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
|
||||||
|
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import Card from '@mui/material/Card';
|
|
||||||
import CardContent from '@mui/material/CardContent';
|
|
||||||
import CardHeader from '@mui/material/CardHeader';
|
|
||||||
import Chip from '@mui/material/Chip';
|
|
||||||
import Divider from '@mui/material/Divider';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
|
||||||
import LinearProgress from '@mui/material/LinearProgress';
|
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import Grid from '@mui/material/Unstable_Grid2';
|
import Grid from '@mui/material/Unstable_Grid2';
|
||||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
import type { RecordModel } from 'pocketbase';
|
||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard';
|
|
||||||
import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House';
|
|
||||||
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
|
||||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
|
||||||
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
|
||||||
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
|
||||||
|
|
||||||
import { config } from '@/config';
|
import { config } from '@/config';
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
import { dayjs } from '@/lib/dayjs';
|
import { logger } from '@/lib/default-logger';
|
||||||
import { PropertyItem } from '@/components/core/property-item';
|
import { pb } from '@/lib/pb';
|
||||||
import { PropertyList } from '@/components/core/property-list';
|
import { toast } from '@/components/core/toaster';
|
||||||
import { Notifications } from '@/components/dashboard/customer/notifications';
|
|
||||||
import { Payments } from '@/components/dashboard/customer/payments';
|
|
||||||
import type { Address } from '@/components/dashboard/customer/shipping-address';
|
|
||||||
import { ShippingAddress } from '@/components/dashboard/customer/shipping-address';
|
|
||||||
|
|
||||||
export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
import ErrorDisplay from '@/components/dashboard/error';
|
||||||
|
|
||||||
|
import { Notifications } from '@/components/dashboard/customer/notifications';
|
||||||
|
import FormLoading from '@/components/loading';
|
||||||
|
import BasicDetailCard from './BasicDetailCard';
|
||||||
|
import TitleCard from './TitleCard';
|
||||||
|
import { defaultCustomer } from '@/components/dashboard/customer/_constants';
|
||||||
|
import type { Customer } from '@/components/dashboard/customer/type.d';
|
||||||
|
import { COL_CUSTOMERS } from '@/constants';
|
||||||
|
|
||||||
export default function Page(): React.JSX.Element {
|
export default function Page(): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
//
|
||||||
|
const { customerId } = useParams<{ customerId: string }>();
|
||||||
|
//
|
||||||
|
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||||
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||||
|
//
|
||||||
|
const [showLessonCategory, setShowLessonCategory] = React.useState<Customer>(defaultCustomer);
|
||||||
|
|
||||||
|
function handleEditClick(): void {
|
||||||
|
router.push(paths.dashboard.customers.edit(showLessonCategory.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (customerId) {
|
||||||
|
pb.collection(COL_CUSTOMERS)
|
||||||
|
.getOne(customerId)
|
||||||
|
.then((model: RecordModel) => {
|
||||||
|
setShowLessonCategory({ ...defaultCustomer, ...model });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(err);
|
||||||
|
toast(t('list.error'));
|
||||||
|
|
||||||
|
setShowError({ show: true, detail: JSON.stringify(err) });
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setShowLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [customerId]);
|
||||||
|
|
||||||
|
// return <>{JSON.stringify({ showError, showLessonCategory }, null, 2)}</>;
|
||||||
|
|
||||||
|
if (showLoading) return <FormLoading />;
|
||||||
|
if (showError.show)
|
||||||
|
return (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t('error.unable-to-process-request')}
|
||||||
|
code="500"
|
||||||
|
details={showError.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -61,244 +101,38 @@ export default function Page(): React.JSX.Element {
|
|||||||
Customers
|
Customers
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
<Stack
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
<Avatar src="/assets/avatar-1.png" sx={{ '--Avatar-size': '64px' }}>
|
spacing={3}
|
||||||
MV
|
sx={{ alignItems: 'flex-start' }}
|
||||||
</Avatar>
|
>
|
||||||
<div>
|
<TitleCard lpModel={showLessonCategory} />
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
|
|
||||||
<Typography variant="h4">Miron Vitold</Typography>
|
|
||||||
<Chip
|
|
||||||
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
|
|
||||||
label="Active"
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Typography color="text.secondary" variant="body1">
|
|
||||||
miron.vitold@domain.com
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
<div>
|
|
||||||
<Button endIcon={<CaretDownIcon />} variant="contained">
|
|
||||||
Action
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Grid container spacing={4}>
|
<Grid
|
||||||
<Grid lg={4} xs={12}>
|
container
|
||||||
|
spacing={4}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
lg={4}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Card>
|
<BasicDetailCard
|
||||||
<CardHeader
|
lpModel={showLessonCategory}
|
||||||
action={
|
handleEditClick={handleEditClick}
|
||||||
<IconButton>
|
/>
|
||||||
<PencilSimpleIcon />
|
<SampleSecurityCard />
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<UserIcon fontSize="var(--Icon-fontSize)" />
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
title="Basic details"
|
|
||||||
/>
|
|
||||||
<PropertyList
|
|
||||||
divider={<Divider />}
|
|
||||||
orientation="vertical"
|
|
||||||
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
|
||||||
>
|
|
||||||
{(
|
|
||||||
[
|
|
||||||
{ key: 'Customer ID', value: <Chip label="USR-001" size="small" variant="soft" /> },
|
|
||||||
{ key: 'Name', value: 'Miron Vitold' },
|
|
||||||
{ key: 'Email', value: 'miron.vitold@domain.com' },
|
|
||||||
{ key: 'Phone', value: '(425) 434-5535' },
|
|
||||||
{ key: 'Company', value: 'Devias IO' },
|
|
||||||
{
|
|
||||||
key: 'Quota',
|
|
||||||
value: (
|
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
|
||||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" />
|
|
||||||
<Typography color="text.secondary" variant="body2">
|
|
||||||
50%
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
] satisfies { key: string; value: React.ReactNode }[]
|
|
||||||
).map(
|
|
||||||
(item): React.JSX.Element => (
|
|
||||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</PropertyList>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
title="Security"
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
<Stack spacing={1}>
|
|
||||||
<div>
|
|
||||||
<Button color="error" variant="contained">
|
|
||||||
Delete account
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Typography color="text.secondary" variant="body2">
|
|
||||||
A deleted customer cannot be restored. All data will be permanently removed.
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid lg={8} xs={12}>
|
<Grid
|
||||||
|
lg={8}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Payments
|
<SamplePaymentCard />
|
||||||
ordersValue={2069.48}
|
<SampleAddressCard />
|
||||||
payments={[
|
<Notifications notifications={SampleNotifications} />
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 500,
|
|
||||||
invoiceId: 'INV-005',
|
|
||||||
status: 'completed',
|
|
||||||
createdAt: dayjs().subtract(5, 'minute').subtract(1, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 324.5,
|
|
||||||
invoiceId: 'INV-004',
|
|
||||||
status: 'refunded',
|
|
||||||
createdAt: dayjs().subtract(21, 'minute').subtract(2, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 746.5,
|
|
||||||
invoiceId: 'INV-003',
|
|
||||||
status: 'completed',
|
|
||||||
createdAt: dayjs().subtract(7, 'minute').subtract(3, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 56.89,
|
|
||||||
invoiceId: 'INV-002',
|
|
||||||
status: 'completed',
|
|
||||||
createdAt: dayjs().subtract(48, 'minute').subtract(4, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: 'USD',
|
|
||||||
amount: 541.59,
|
|
||||||
invoiceId: 'INV-001',
|
|
||||||
status: 'completed',
|
|
||||||
createdAt: dayjs().subtract(31, 'minute').subtract(5, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
refundsValue={324.5}
|
|
||||||
totalOrders={5}
|
|
||||||
/>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
action={
|
|
||||||
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
title="Billing details"
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
<Card sx={{ borderRadius: 1 }} variant="outlined">
|
|
||||||
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}>
|
|
||||||
{(
|
|
||||||
[
|
|
||||||
{ key: 'Credit card', value: '**** 4142' },
|
|
||||||
{ key: 'Country', value: 'United States' },
|
|
||||||
{ key: 'State', value: 'Michigan' },
|
|
||||||
{ key: 'City', value: 'Southfield' },
|
|
||||||
{ key: 'Address', value: '1721 Bartlett Avenue, 48034' },
|
|
||||||
{ key: 'Tax ID', value: 'EU87956621' },
|
|
||||||
] satisfies { key: string; value: React.ReactNode }[]
|
|
||||||
).map(
|
|
||||||
(item): React.JSX.Element => (
|
|
||||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</PropertyList>
|
|
||||||
</Card>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
action={
|
|
||||||
<Button color="secondary" startIcon={<PlusIcon />}>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<HouseIcon fontSize="var(--Icon-fontSize)" />
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
title="Shipping addresses"
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
<Grid container spacing={3}>
|
|
||||||
{(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: 'ADR-001',
|
|
||||||
country: 'United States',
|
|
||||||
state: 'Michigan',
|
|
||||||
city: 'Lansing',
|
|
||||||
zipCode: '48933',
|
|
||||||
street: '480 Haven Lane',
|
|
||||||
primary: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ADR-002',
|
|
||||||
country: 'United States',
|
|
||||||
state: 'Missouri',
|
|
||||||
city: 'Springfield',
|
|
||||||
zipCode: '65804',
|
|
||||||
street: '4807 Lighthouse Drive',
|
|
||||||
},
|
|
||||||
] satisfies Address[]
|
|
||||||
).map((address) => (
|
|
||||||
<Grid key={address.id} md={6} xs={12}>
|
|
||||||
<ShippingAddress address={address} />
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Notifications
|
|
||||||
notifications={[
|
|
||||||
{
|
|
||||||
id: 'EV-002',
|
|
||||||
type: 'Refund request approved',
|
|
||||||
status: 'pending',
|
|
||||||
createdAt: dayjs().subtract(34, 'minute').subtract(5, 'hour').subtract(3, 'day').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'EV-001',
|
|
||||||
type: 'Order confirmation',
|
|
||||||
status: 'delivered',
|
|
||||||
createdAt: dayjs().subtract(49, 'minute').subtract(11, 'hour').subtract(4, 'day').toDate(),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
49
002_source/cms/src/app/dashboard/customers/_GUIDELINES.md
Normal file
49
002_source/cms/src/app/dashboard/customers/_GUIDELINES.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# GUIDELINES
|
||||||
|
|
||||||
|
this folder is part of nextjs typescript project and containing page definition for `Customer` / `Customers` record:
|
||||||
|
|
||||||
|
- list (./page.tsx)
|
||||||
|
- view (./[customerId]/page.tsx)
|
||||||
|
- create (./create/page.tsx)
|
||||||
|
- edit (./[customerId]/page.tsx)
|
||||||
|
- translation provided by react-i18next
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
simple template:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/app/dashboard/customers/page.tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
// RULES:
|
||||||
|
// contains list page for customers (Customers)
|
||||||
|
// contain definition to collection only
|
||||||
|
//
|
||||||
|
import statements here ...
|
||||||
|
...
|
||||||
|
...
|
||||||
|
...
|
||||||
|
|
||||||
|
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||||
|
// ...content
|
||||||
|
// use direct return of pb.collection (e.g. return pb.collection(xxx))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{* page content *}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string };
|
||||||
|
}
|
||||||
|
```
|
@@ -0,0 +1,11 @@
|
|||||||
|
# task
|
||||||
|
|
||||||
|
## instruction
|
||||||
|
|
||||||
|
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/_helloworld/page.tsx`
|
||||||
|
|
||||||
|
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_types/edit/[typeId]/page.tsx`
|
||||||
|
|
||||||
|
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_categories/edit/page.tsx`
|
||||||
|
|
||||||
|
please draft a tsx for showing error to user thanks,
|
@@ -0,0 +1,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import RouterLink from 'next/link';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { paths } from '@/paths';
|
||||||
|
import { CrCategoryEditForm } from '@/components/dashboard/cr/categories/cr-category-edit-form';
|
||||||
|
import { CustomerEditForm } from '@/components/dashboard/customer/customer-edit-form';
|
||||||
|
|
||||||
|
export default function Page(): React.JSX.Element {
|
||||||
|
const { t } = useTranslation(['lp_categories']);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// console.log('helloworld');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxWidth: 'var(--Content-maxWidth)',
|
||||||
|
m: 'var(--Content-margin)',
|
||||||
|
p: 'var(--Content-padding)',
|
||||||
|
width: 'var(--Content-width)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
color="text.primary"
|
||||||
|
component={RouterLink}
|
||||||
|
href={paths.dashboard.cr_categories.list}
|
||||||
|
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||||
|
variant="subtitle2"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||||
|
{t('edit.title')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant="h4">{t('edit.title')}</Typography>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
<CustomerEditForm />
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,7 +1,14 @@
|
|||||||
|
// src/app/dashboard/customers/page.tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
// RULES:
|
||||||
|
// contains list page for customers (Customers)
|
||||||
|
// contain definition to collection only
|
||||||
|
//
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
// import type { Metadata } from 'next';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { COL_CUSTOMERS } from '@/constants';
|
||||||
|
import { LoadingButton } from '@mui/lab';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Card from '@mui/material/Card';
|
import Card from '@mui/material/Card';
|
||||||
@@ -9,184 +16,140 @@ import Divider from '@mui/material/Divider';
|
|||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||||
|
import type { ListResult, RecordModel } from 'pocketbase';
|
||||||
|
|
||||||
import { config } from '@/config';
|
import { config } from '@/config';
|
||||||
import { dayjs } from '@/lib/dayjs';
|
|
||||||
import { CustomersFilters } from '@/components/dashboard/customer/customers-filters';
|
import { CustomersFilters } from '@/components/dashboard/customer/customers-filters';
|
||||||
import type { Filters } from '@/components/dashboard/customer/customers-filters';
|
// import type { Filters } from '@/components/dashboard/customer/customers-filters';
|
||||||
import { CustomersPagination } from '@/components/dashboard/customer/customers-pagination';
|
import { CustomersPagination } from '@/components/dashboard/customer/customers-pagination';
|
||||||
import { CustomersSelectionProvider } from '@/components/dashboard/customer/customers-selection-context';
|
import { CustomersSelectionProvider } from '@/components/dashboard/customer/customers-selection-context';
|
||||||
import { CustomersTable } from '@/components/dashboard/customer/customers-table';
|
import { CustomersTable } from '@/components/dashboard/customer/customers-table';
|
||||||
import type { Customer } from '@/components/dashboard/customer/customers-table';
|
import type { Customer, Filters } from '@/components/dashboard/customer/type.d';
|
||||||
|
import { SampleCustomers } from './SampleCustomers';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
// export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
import { paths } from '@/paths';
|
||||||
|
import isDevelopment from '@/lib/check-is-development';
|
||||||
const customers = [
|
import { logger } from '@/lib/default-logger';
|
||||||
{
|
import { pb } from '@/lib/pb';
|
||||||
id: 'USR-005',
|
import { toast } from '@/components/core/toaster';
|
||||||
name: 'Fran Perez',
|
import ErrorDisplay from '@/components/dashboard/error';
|
||||||
avatar: '/assets/avatar-5.png',
|
import { defaultCustomer } from '@/components/dashboard/customer/_constants';
|
||||||
email: 'fran.perez@domain.com',
|
import FormLoading from '@/components/loading';
|
||||||
phone: '(815) 704-0045',
|
|
||||||
quota: 50,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: dayjs().subtract(1, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USR-004',
|
|
||||||
name: 'Penjani Inyene',
|
|
||||||
avatar: '/assets/avatar-4.png',
|
|
||||||
email: 'penjani.inyene@domain.com',
|
|
||||||
phone: '(803) 937-8925',
|
|
||||||
quota: 100,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: dayjs().subtract(3, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USR-003',
|
|
||||||
name: 'Carson Darrin',
|
|
||||||
avatar: '/assets/avatar-3.png',
|
|
||||||
email: 'carson.darrin@domain.com',
|
|
||||||
phone: '(715) 278-5041',
|
|
||||||
quota: 10,
|
|
||||||
status: 'blocked',
|
|
||||||
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USR-002',
|
|
||||||
name: 'Siegbert Gottfried',
|
|
||||||
avatar: '/assets/avatar-2.png',
|
|
||||||
email: 'siegbert.gottfried@domain.com',
|
|
||||||
phone: '(603) 766-0431',
|
|
||||||
quota: 0,
|
|
||||||
status: 'pending',
|
|
||||||
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USR-001',
|
|
||||||
name: 'Miron Vitold',
|
|
||||||
avatar: '/assets/avatar-1.png',
|
|
||||||
email: 'miron.vitold@domain.com',
|
|
||||||
phone: '(425) 434-5535',
|
|
||||||
quota: 50,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USR-005',
|
|
||||||
name: 'Fran Perez',
|
|
||||||
avatar: '/assets/avatar-5.png',
|
|
||||||
email: 'fran.perez@domain.com',
|
|
||||||
phone: '(815) 704-0045',
|
|
||||||
quota: 50,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: dayjs().subtract(1, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USR-004',
|
|
||||||
name: 'Penjani Inyene',
|
|
||||||
avatar: '/assets/avatar-4.png',
|
|
||||||
email: 'penjani.inyene@domain.com',
|
|
||||||
phone: '(803) 937-8925',
|
|
||||||
quota: 100,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: dayjs().subtract(3, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USR-003',
|
|
||||||
name: 'Carson Darrin',
|
|
||||||
avatar: '/assets/avatar-3.png',
|
|
||||||
email: 'carson.darrin@domain.com',
|
|
||||||
phone: '(715) 278-5041',
|
|
||||||
quota: 10,
|
|
||||||
status: 'blocked',
|
|
||||||
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USR-002',
|
|
||||||
name: 'Siegbert Gottfried',
|
|
||||||
avatar: '/assets/avatar-2.png',
|
|
||||||
email: 'siegbert.gottfried@domain.com',
|
|
||||||
phone: '(603) 766-0431',
|
|
||||||
quota: 0,
|
|
||||||
status: 'pending',
|
|
||||||
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USR-001',
|
|
||||||
name: 'Miron Vitold',
|
|
||||||
avatar: '/assets/avatar-1.png',
|
|
||||||
email: 'miron.vitold@domain.com',
|
|
||||||
phone: '(425) 434-5535',
|
|
||||||
quota: 50,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USR-005',
|
|
||||||
name: 'Fran Perez',
|
|
||||||
avatar: '/assets/avatar-5.png',
|
|
||||||
email: 'fran.perez@domain.com',
|
|
||||||
phone: '(815) 704-0045',
|
|
||||||
quota: 50,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: dayjs().subtract(1, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USR-004',
|
|
||||||
name: 'Penjani Inyene',
|
|
||||||
avatar: '/assets/avatar-4.png',
|
|
||||||
email: 'penjani.inyene@domain.com',
|
|
||||||
phone: '(803) 937-8925',
|
|
||||||
quota: 100,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: dayjs().subtract(3, 'hour').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USR-003',
|
|
||||||
name: 'Carson Darrin',
|
|
||||||
avatar: '/assets/avatar-3.png',
|
|
||||||
email: 'carson.darrin@domain.com',
|
|
||||||
phone: '(715) 278-5041',
|
|
||||||
quota: 10,
|
|
||||||
status: 'blocked',
|
|
||||||
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USR-002',
|
|
||||||
name: 'Siegbert Gottfried',
|
|
||||||
avatar: '/assets/avatar-2.png',
|
|
||||||
email: 'siegbert.gottfried@domain.com',
|
|
||||||
phone: '(603) 766-0431',
|
|
||||||
quota: 0,
|
|
||||||
status: 'pending',
|
|
||||||
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USR-001',
|
|
||||||
name: 'Miron Vitold',
|
|
||||||
avatar: '/assets/avatar-1.png',
|
|
||||||
email: 'miron.vitold@domain.com',
|
|
||||||
phone: '(425) 434-5535',
|
|
||||||
quota: 50,
|
|
||||||
status: 'active',
|
|
||||||
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
|
||||||
},
|
|
||||||
] satisfies Customer[];
|
|
||||||
|
|
||||||
interface PageProps {
|
|
||||||
searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||||
|
const { t } = useTranslation(['customers']);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const { email, phone, sortDir, status } = searchParams;
|
const { email, phone, sortDir, status } = searchParams;
|
||||||
|
|
||||||
const sortedCustomers = applySort(customers, sortDir);
|
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<Customer[]>([]);
|
||||||
const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status });
|
//
|
||||||
|
|
||||||
|
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
||||||
|
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||||
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||||
|
//
|
||||||
|
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||||
|
//
|
||||||
|
const [f, setF] = React.useState<Customer[]>([]);
|
||||||
|
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||||
|
//
|
||||||
|
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||||
|
const [listOption, setListOption] = React.useState({});
|
||||||
|
const [listSort, setListSort] = React.useState({});
|
||||||
|
|
||||||
|
//
|
||||||
|
// const sortedCustomers = applySort(SampleCustomers, sortDir);
|
||||||
|
// const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status });
|
||||||
|
|
||||||
|
//
|
||||||
|
const reloadRows = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const models: ListResult<RecordModel> = await pb
|
||||||
|
.collection(COL_CUSTOMERS)
|
||||||
|
.getList(currentPage + 1, rowsPerPage, listOption);
|
||||||
|
const { items, totalItems } = models;
|
||||||
|
const tempLessonTypes: Customer[] = items.map((lt) => {
|
||||||
|
return { ...defaultCustomer, ...lt };
|
||||||
|
});
|
||||||
|
|
||||||
|
setLessonCategoriesData(tempLessonTypes);
|
||||||
|
setRecordCount(totalItems);
|
||||||
|
setF(tempLessonTypes);
|
||||||
|
// console.log({ currentPage, f });
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
logger.error(error);
|
||||||
|
setShowError({
|
||||||
|
//
|
||||||
|
show: true,
|
||||||
|
detail: JSON.stringify(error, null, 2),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setShowLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [lastListOption, setLastListOption] = React.useState({});
|
||||||
|
const isFirstRun = React.useRef(false);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isFirstRun.current) {
|
||||||
|
isFirstRun.current = true;
|
||||||
|
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
||||||
|
// reset page number as tab changes
|
||||||
|
setLastListOption(listOption);
|
||||||
|
setCurrentPage(0);
|
||||||
|
void reloadRows();
|
||||||
|
} else {
|
||||||
|
void reloadRows();
|
||||||
|
}
|
||||||
|
}, [currentPage, rowsPerPage, listOption]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log('helloworld');
|
let tempFilter = [],
|
||||||
}, []);
|
tempSortDir = '';
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
tempFilter.push(`status = "${status}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDir) {
|
||||||
|
tempSortDir = `-created`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
tempFilter.push(`email ~ "%${email}%"`);
|
||||||
|
}
|
||||||
|
if (phone) {
|
||||||
|
tempFilter.push(`phone ~ "%${phone}%"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let preFinalListOption = {};
|
||||||
|
if (tempFilter.length > 0) {
|
||||||
|
preFinalListOption = { filter: tempFilter.join(' && ') };
|
||||||
|
}
|
||||||
|
if (tempSortDir.length > 0) {
|
||||||
|
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
|
||||||
|
}
|
||||||
|
setListOption(preFinalListOption);
|
||||||
|
// setListOption({
|
||||||
|
// filter: tempFilter.join(' && '),
|
||||||
|
// sort: tempSortDir,
|
||||||
|
// //
|
||||||
|
// });
|
||||||
|
}, [sortDir, email, phone, status]);
|
||||||
|
|
||||||
|
if (showLoading) return <FormLoading />;
|
||||||
|
|
||||||
|
if (showError.show)
|
||||||
|
return (
|
||||||
|
<ErrorDisplay
|
||||||
|
message={t('error.unable-to-process-request')}
|
||||||
|
code={-1}
|
||||||
|
details={showError.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -204,35 +167,50 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
sx={{ alignItems: 'flex-start' }}
|
sx={{ alignItems: 'flex-start' }}
|
||||||
>
|
>
|
||||||
<Box sx={{ flex: '1 1 auto' }}>
|
<Box sx={{ flex: '1 1 auto' }}>
|
||||||
<Typography variant="h4">Customers</Typography>
|
<Typography variant="h4">{t('list.title')}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
<Button
|
<LoadingButton
|
||||||
|
loading={isLoadingAddPage}
|
||||||
|
onClick={(): void => {
|
||||||
|
setIsLoadingAddPage(true);
|
||||||
|
router.push(paths.dashboard.customers.create);
|
||||||
|
}}
|
||||||
startIcon={<PlusIcon />}
|
startIcon={<PlusIcon />}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
>
|
>
|
||||||
Add
|
{t('list.add')}
|
||||||
</Button>
|
</LoadingButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
<CustomersSelectionProvider customers={filteredCustomers}>
|
<CustomersSelectionProvider customers={f}>
|
||||||
<Card>
|
<Card>
|
||||||
<CustomersFilters
|
<CustomersFilters
|
||||||
filters={{ email, phone, status }}
|
filters={{ email, phone, status }}
|
||||||
|
fullData={lessonCategoriesData}
|
||||||
sortDir={sortDir}
|
sortDir={sortDir}
|
||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Box sx={{ overflowX: 'auto' }}>
|
<Box sx={{ overflowX: 'auto' }}>
|
||||||
<CustomersTable rows={filteredCustomers} />
|
<CustomersTable
|
||||||
|
reloadRows={reloadRows}
|
||||||
|
rows={f}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider />
|
<Divider />
|
||||||
<CustomersPagination
|
<CustomersPagination
|
||||||
count={filteredCustomers.length + 100}
|
count={recordCount}
|
||||||
page={0}
|
page={currentPage}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
setPage={setCurrentPage}
|
||||||
|
setRowsPerPage={setRowsPerPage}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</CustomersSelectionProvider>
|
</CustomersSelectionProvider>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
|
||||||
|
<pre>{JSON.stringify(f, null, 2)}</pre>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -272,3 +250,7 @@ function applyFilters(row: Customer[], { email, phone, status }: Filters): Custo
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string };
|
||||||
|
}
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
// RULES:
|
||||||
|
// contains list page for lesson_categories (LessonCategories)
|
||||||
|
// contain definition to collection only
|
||||||
|
//
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { COL_LESSON_CATEGORIES } from '@/constants';
|
import { COL_LESSON_CATEGORIES } from '@/constants';
|
||||||
@@ -14,6 +18,7 @@ import type { ListResult, RecordModel } from 'pocketbase';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
|
import isDevelopment from '@/lib/check-is-development';
|
||||||
import { logger } from '@/lib/default-logger';
|
import { logger } from '@/lib/default-logger';
|
||||||
import { pb } from '@/lib/pb';
|
import { pb } from '@/lib/pb';
|
||||||
import { toast } from '@/components/core/toaster';
|
import { toast } from '@/components/core/toaster';
|
||||||
@@ -24,7 +29,7 @@ import type { Filters } from '@/components/dashboard/lesson_category/lesson-cate
|
|||||||
import { LessonCategoriesPagination } from '@/components/dashboard/lesson_category/lesson-categories-pagination';
|
import { LessonCategoriesPagination } from '@/components/dashboard/lesson_category/lesson-categories-pagination';
|
||||||
import { LessonCategoriesSelectionProvider } from '@/components/dashboard/lesson_category/lesson-categories-selection-context';
|
import { LessonCategoriesSelectionProvider } from '@/components/dashboard/lesson_category/lesson-categories-selection-context';
|
||||||
import { LessonCategoriesTable } from '@/components/dashboard/lesson_category/lesson-categories-table';
|
import { LessonCategoriesTable } from '@/components/dashboard/lesson_category/lesson-categories-table';
|
||||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||||
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
||||||
import FormLoading from '@/components/loading';
|
import FormLoading from '@/components/loading';
|
||||||
|
|
||||||
@@ -39,60 +44,138 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
|
|
||||||
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
||||||
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||||
const [showError, setShowError] = React.useState<boolean>(false);
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||||
|
//
|
||||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||||
//
|
//
|
||||||
const [f, setF] = React.useState<LessonCategory[]>([]);
|
const [f, setF] = React.useState<LessonCategory[]>([]);
|
||||||
const [currentPage, setCurrentPage] = React.useState<number>(1);
|
const [currentPage, setCurrentPage] = React.useState<number>(1);
|
||||||
//
|
//
|
||||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||||
|
const [listOption, setListOption] = React.useState({});
|
||||||
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
|
const [listSort, setListSort] = React.useState({});
|
||||||
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status: status });
|
|
||||||
|
|
||||||
//
|
//
|
||||||
const reloadRows = () => {
|
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
|
||||||
setShowLoading(true);
|
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status });
|
||||||
|
|
||||||
pb.collection(COL_LESSON_CATEGORIES)
|
//
|
||||||
.getList(currentPage, rowsPerPage, {})
|
const reloadRows = async (): Promise<void> => {
|
||||||
.then((lessonCategories: ListResult<RecordModel>) => {
|
try {
|
||||||
// console.log(lessonTypes);
|
const models: ListResult<RecordModel> = await pb
|
||||||
const { items, page, perPage, totalItems, totalPages } = lessonCategories;
|
.collection(COL_LESSON_CATEGORIES)
|
||||||
const tempLessonCategories: LessonCategory[] = items.map((item) => {
|
.getList(currentPage + 1, rowsPerPage, listOption);
|
||||||
return { ...defaultLessonCategory, ...item };
|
const { items, totalItems } = models;
|
||||||
});
|
const tempLessonTypes: LessonCategory[] = items.map((lt) => {
|
||||||
|
return { ...defaultLessonCategory, ...lt };
|
||||||
setLessonCategoriesData(tempLessonCategories);
|
|
||||||
setRecordCount(totalItems);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.error(err);
|
|
||||||
toast(t('dashboard.lessonTypes.list.error'));
|
|
||||||
setShowError(true);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setShowLoading(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setLessonCategoriesData(tempLessonTypes);
|
||||||
|
setRecordCount(totalItems);
|
||||||
|
setF(tempLessonTypes);
|
||||||
|
// console.log({ currentPage, f });
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
logger.error(error);
|
||||||
|
setShowError({
|
||||||
|
//
|
||||||
|
show: true,
|
||||||
|
detail: JSON.stringify(error, null, 2),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setShowLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pb.collection(COL_LESSON_CATEGORIES)
|
||||||
|
// .getList(currentPage, rowsPerPage, listOption)
|
||||||
|
// .then((lessonCategories: ListResult<RecordModel>) => {
|
||||||
|
// // console.log(lessonTypes);
|
||||||
|
// const { items, page, perPage, totalItems, totalPages } = lessonCategories;
|
||||||
|
// const tempLessonCategories: LessonCategory[] = items.map((item) => {
|
||||||
|
// return { ...defaultLessonCategory, ...item };
|
||||||
|
// });
|
||||||
|
|
||||||
|
// setLessonCategoriesData(tempLessonCategories);
|
||||||
|
// setRecordCount(totalItems);
|
||||||
|
// setF(tempLessonCategories);
|
||||||
|
// // console.log({ currentPage, f });
|
||||||
|
// })
|
||||||
|
// .catch((error) => {
|
||||||
|
// logger.error(error);
|
||||||
|
// setShowError({
|
||||||
|
// //
|
||||||
|
// show: true,
|
||||||
|
// detail: JSON.stringify(error, null, 2),
|
||||||
|
// });
|
||||||
|
// })
|
||||||
|
// .finally(() => {
|
||||||
|
// setShowLoading(false);
|
||||||
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [lastListOption, setLastListOption] = React.useState({});
|
||||||
|
const isFirstRun = React.useRef(false);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
reloadRows();
|
if (!isFirstRun.current) {
|
||||||
}, []);
|
isFirstRun.current = true;
|
||||||
|
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
||||||
|
// reset page number as tab changes
|
||||||
|
setLastListOption(listOption);
|
||||||
|
setCurrentPage(0);
|
||||||
|
void reloadRows();
|
||||||
|
} else {
|
||||||
|
void reloadRows();
|
||||||
|
}
|
||||||
|
}, [currentPage, rowsPerPage, listOption]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let tempFilter = [],
|
||||||
|
tempSortDir = '';
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
tempFilter.push(`visible = "${visible}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDir) {
|
||||||
|
tempSortDir = `-created`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
tempFilter.push(`name ~ "%${name}%"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
tempFilter.push(`type ~ "%${type}%"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let preFinalListOption = {};
|
||||||
|
if (tempFilter.length > 0) {
|
||||||
|
preFinalListOption = { filter: tempFilter.join(' && ') };
|
||||||
|
}
|
||||||
|
if (tempSortDir.length > 0) {
|
||||||
|
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
|
||||||
|
}
|
||||||
|
setListOption(preFinalListOption);
|
||||||
|
// setListOption({
|
||||||
|
// filter: tempFilter.join(' && '),
|
||||||
|
// sort: tempSortDir,
|
||||||
|
// //
|
||||||
|
// });
|
||||||
|
}, [visible, sortDir, name, type]);
|
||||||
|
|
||||||
|
// return <>helloworld</>;
|
||||||
|
|
||||||
if (showLoading) return <FormLoading />;
|
if (showLoading) return <FormLoading />;
|
||||||
|
|
||||||
if (showError)
|
if (showError.show)
|
||||||
return (
|
return (
|
||||||
<ErrorDisplay
|
<ErrorDisplay
|
||||||
message={t('error.unable-to-process-request', { ns: 'common' })}
|
message={t('error.unable-to-process-request')}
|
||||||
code="500"
|
code="500"
|
||||||
details={t('error.detailed-error-information', { ns: 'common' })}
|
details={showError.detail}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// return <pre>{JSON.stringify(lessonCategoriesData, null, 2)}</pre>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -103,7 +186,11 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack spacing={4}>
|
<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' }}>
|
<Box sx={{ flex: '1 1 auto' }}>
|
||||||
<Typography variant="h4">{t('list.title')}</Typography>
|
<Typography variant="h4">{t('list.title')}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -117,6 +204,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
startIcon={<PlusIcon />}
|
startIcon={<PlusIcon />}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
>
|
>
|
||||||
|
{/* add new lesson type */}
|
||||||
{t('list.add')}
|
{t('list.add')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -130,13 +218,25 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Box sx={{ overflowX: 'auto' }}>
|
<Box sx={{ overflowX: 'auto' }}>
|
||||||
<LessonCategoriesTable reloadRows={reloadRows} rows={filteredLessonCategories} />
|
<LessonCategoriesTable
|
||||||
|
reloadRows={reloadRows}
|
||||||
|
rows={f}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider />
|
<Divider />
|
||||||
<LessonCategoriesPagination count={filteredLessonCategories.length + 100} page={0} />
|
<LessonCategoriesPagination
|
||||||
|
count={recordCount}
|
||||||
|
page={currentPage}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
setPage={setCurrentPage}
|
||||||
|
setRowsPerPage={setRowsPerPage}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</LessonCategoriesSelectionProvider>
|
</LessonCategoriesSelectionProvider>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
|
||||||
|
<pre>{JSON.stringify(f, null, 2)}</pre>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -153,19 +253,19 @@ function applySort(row: LessonCategory[], sortDir: 'asc' | 'desc' | undefined):
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyFilters(row: LessonCategory[], { email, phone, status }: Filters): LessonCategory[] {
|
function applyFilters(row: LessonCategory[], { email, phone, status, name, visible }: Filters): LessonCategory[] {
|
||||||
return row.filter((item) => {
|
return row.filter((item) => {
|
||||||
// if (email) {
|
if (email) {
|
||||||
// if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
|
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
|
||||||
// return false;
|
return false;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if (phone) {
|
if (phone) {
|
||||||
// if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
|
if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
|
||||||
// return false;
|
return false;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
if (item.status !== status) {
|
if (item.status !== status) {
|
||||||
@@ -173,6 +273,18 @@ function applyFilters(row: LessonCategory[], { email, phone, status }: Filters):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
if (!item.name?.toLowerCase().includes(name.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -186,6 +298,5 @@ interface PageProps {
|
|||||||
name?: string;
|
name?: string;
|
||||||
visible?: string;
|
visible?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
//
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -6,9 +6,9 @@ import { useParams, useRouter } from 'next/navigation';
|
|||||||
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
|
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
|
||||||
import BasicDetailCard from '@/app/dashboard/Sample/BasicDetailCard';
|
import BasicDetailCard from '@/app/dashboard/Sample/BasicDetailCard';
|
||||||
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
|
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
|
||||||
import SamplePaymentCard from '@/app/dashboard/Sample/SamplePaymentCard';
|
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
|
||||||
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
|
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
|
||||||
import SampleTitleCard from '@/app/dashboard/Sample/SampleTitleCard';
|
import SampleTitleCard from '@/app/dashboard/Sample/TitleCard';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// RULES:
|
// RULES:
|
||||||
// contains list page for lp_categories (QuizLPCategories)
|
// contains list page for lesson_types (LessonTypes)
|
||||||
// contain definition to collection only
|
// contain definition to collection only
|
||||||
//
|
//
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
@@ -18,6 +18,7 @@ import type { ListResult, RecordModel } from 'pocketbase';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
|
import isDevelopment from '@/lib/check-is-development';
|
||||||
import { logger } from '@/lib/default-logger';
|
import { logger } from '@/lib/default-logger';
|
||||||
import { pb } from '@/lib/pb';
|
import { pb } from '@/lib/pb';
|
||||||
import { toast } from '@/components/core/toaster';
|
import { toast } from '@/components/core/toaster';
|
||||||
@@ -43,8 +44,10 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||||
//
|
//
|
||||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||||
|
//
|
||||||
const [f, setF] = React.useState<LessonType[]>([]);
|
const [f, setF] = React.useState<LessonType[]>([]);
|
||||||
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||||
|
//
|
||||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||||
const [listOption, setListOption] = React.useState({});
|
const [listOption, setListOption] = React.useState({});
|
||||||
const [listSort, setListSort] = React.useState({});
|
const [listSort, setListSort] = React.useState({});
|
||||||
@@ -63,17 +66,33 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
setLessonTypesData(tempLessonTypes);
|
setLessonTypesData(tempLessonTypes);
|
||||||
setRecordCount(totalItems);
|
setRecordCount(totalItems);
|
||||||
setF(tempLessonTypes);
|
setF(tempLessonTypes);
|
||||||
|
// console.log({ currentPage, f });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//
|
//
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
setShowError({ show: true, detail: JSON.stringify(error, null, 2) });
|
setShowError({
|
||||||
|
//
|
||||||
|
show: true,
|
||||||
|
detail: JSON.stringify(error, null, 2),
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setShowLoading(false);
|
setShowLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [lastListOption, setLastListOption] = React.useState({});
|
||||||
|
const isFirstRun = React.useRef(false);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
void reloadRows();
|
if (!isFirstRun.current) {
|
||||||
|
isFirstRun.current = true;
|
||||||
|
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
||||||
|
// reset page number as tab changes
|
||||||
|
setLastListOption(listOption);
|
||||||
|
setCurrentPage(0);
|
||||||
|
void reloadRows();
|
||||||
|
} else {
|
||||||
|
void reloadRows();
|
||||||
|
}
|
||||||
}, [currentPage, rowsPerPage, listOption]);
|
}, [currentPage, rowsPerPage, listOption]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -96,16 +115,26 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
tempFilter.push(`type ~ "%${type}%"`);
|
tempFilter.push(`type ~ "%${type}%"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setListOption({
|
let preFinalListOption = {};
|
||||||
filter: tempFilter.join(' && '),
|
if (tempFilter.length > 0) {
|
||||||
sort: tempSortDir,
|
preFinalListOption = { filter: tempFilter.join(' && ') };
|
||||||
//
|
}
|
||||||
});
|
if (tempSortDir.length > 0) {
|
||||||
|
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
|
||||||
|
}
|
||||||
|
setListOption(preFinalListOption);
|
||||||
|
// setListOption({
|
||||||
|
// filter: tempFilter.join(' && '),
|
||||||
|
// sort: tempSortDir,
|
||||||
|
// //
|
||||||
|
// });
|
||||||
}, [visible, sortDir, name, type]);
|
}, [visible, sortDir, name, type]);
|
||||||
|
|
||||||
if (f.length === 0 || showLoading) return <FormLoading />;
|
// return <>helloworld</>;
|
||||||
|
|
||||||
if (showError)
|
if (showLoading) return <FormLoading />;
|
||||||
|
|
||||||
|
if (showError.show)
|
||||||
return (
|
return (
|
||||||
<ErrorDisplay
|
<ErrorDisplay
|
||||||
message={t('error.unable-to-process-request')}
|
message={t('error.unable-to-process-request')}
|
||||||
@@ -143,6 +172,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
>
|
>
|
||||||
{/* add new lesson type */}
|
{/* add new lesson type */}
|
||||||
|
{/* TODO: refactor translations.json */}
|
||||||
{t('dashboard.lessonTypes.add')}
|
{t('dashboard.lessonTypes.add')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -153,14 +183,12 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
filters={{ email, phone, status, name, visible, type }}
|
filters={{ email, phone, status, name, visible, type }}
|
||||||
fullData={lessonTypesData}
|
fullData={lessonTypesData}
|
||||||
sortDir={sortDir}
|
sortDir={sortDir}
|
||||||
//
|
|
||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Box sx={{ overflowX: 'auto' }}>
|
<Box sx={{ overflowX: 'auto' }}>
|
||||||
<LessonTypesTable
|
<LessonTypesTable
|
||||||
reloadRows={reloadRows}
|
reloadRows={reloadRows}
|
||||||
rows={f}
|
rows={f}
|
||||||
//
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider />
|
<Divider />
|
||||||
@@ -174,6 +202,9 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
</Card>
|
</Card>
|
||||||
</LessonTypesSelectionProvider>
|
</LessonTypesSelectionProvider>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
|
||||||
|
<pre>{JSON.stringify(f, null, 2)}</pre>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -235,6 +266,5 @@ interface PageProps {
|
|||||||
name?: string;
|
name?: string;
|
||||||
visible?: string;
|
visible?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
//
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
3587
002_source/cms/src/app/dashboard/lp/_repomix.md
Normal file
3587
002_source/cms/src/app/dashboard/lp/_repomix.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import { PropertyItem } from '@/components/core/property-item';
|
import { PropertyItem } from '@/components/core/property-item';
|
||||||
import { PropertyList } from '@/components/core/property-list';
|
import { PropertyList } from '@/components/core/property-list';
|
||||||
import { LpCategory } from '@/components/dashboard/lp_categories/type';
|
import { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||||
|
|
||||||
export default function BasicDetailCard({
|
export default function BasicDetailCard({
|
||||||
lpModel: model,
|
lpModel: model,
|
||||||
|
@@ -10,7 +10,7 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
|
|||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { LpCategory } from '@/components/dashboard/lp_categories/type';
|
import { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||||
|
|
||||||
function getImageUrlFrRecord(record: LpCategory): string {
|
function getImageUrlFrRecord(record: LpCategory): string {
|
||||||
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
||||||
|
@@ -5,8 +5,8 @@ import RouterLink from 'next/link';
|
|||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
|
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
|
||||||
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
|
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
|
||||||
import SamplePaymentCard from '@/app/dashboard/Sample/SamplePaymentCard';
|
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
|
||||||
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
|
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
|
||||||
import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
|
import { COL_QUIZ_LP_CATEGORIES } from '@/constants';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
@@ -21,9 +21,9 @@ import { logger } from '@/lib/default-logger';
|
|||||||
import { pb } from '@/lib/pb';
|
import { pb } from '@/lib/pb';
|
||||||
import { toast } from '@/components/core/toaster';
|
import { toast } from '@/components/core/toaster';
|
||||||
import ErrorDisplay from '@/components/dashboard/error';
|
import ErrorDisplay from '@/components/dashboard/error';
|
||||||
import { defaultLpCategory } from '@/components/dashboard/lp_categories/_constants.ts';
|
import { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants.ts';
|
||||||
import { Notifications } from '@/components/dashboard/lp_categories/notifications';
|
import { Notifications } from '@/components/dashboard/lp/categories/notifications';
|
||||||
import type { LpCategory } from '@/components/dashboard/lp_categories/type';
|
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||||
import FormLoading from '@/components/loading';
|
import FormLoading from '@/components/loading';
|
||||||
|
|
||||||
import BasicDetailCard from './BasicDetailCard';
|
import BasicDetailCard from './BasicDetailCard';
|
||||||
|
@@ -13,7 +13,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
import { LpCategoryCreateForm } from '@/components/dashboard/lp_categories/lp-category-create-form';
|
import { LpCategoryCreateForm } from '@/components/dashboard/lp/categories/lp-category-create-form';
|
||||||
|
|
||||||
export default function Page(): React.JSX.Element {
|
export default function Page(): React.JSX.Element {
|
||||||
// RULES: follow the name of page directory
|
// RULES: follow the name of page directory
|
||||||
|
@@ -10,7 +10,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
import { LpCategoryEditForm } from '@/components/dashboard/lp_categories/lp-category-edit-form';
|
import { LpCategoryEditForm } from '@/components/dashboard/lp/categories/lp-category-edit-form';
|
||||||
|
|
||||||
export default function Page(): React.JSX.Element {
|
export default function Page(): React.JSX.Element {
|
||||||
const { t } = useTranslation(['lp_categories']);
|
const { t } = useTranslation(['lp_categories']);
|
||||||
|
@@ -23,29 +23,30 @@ import { logger } from '@/lib/default-logger';
|
|||||||
import { pb } from '@/lib/pb';
|
import { pb } from '@/lib/pb';
|
||||||
import { toast } from '@/components/core/toaster';
|
import { toast } from '@/components/core/toaster';
|
||||||
import ErrorDisplay from '@/components/dashboard/error';
|
import ErrorDisplay from '@/components/dashboard/error';
|
||||||
import { defaultLpCategory } from '@/components/dashboard/lp_categories/_constants';
|
import { defaultLpCategory } from '@/components/dashboard/lp/categories/_constants';
|
||||||
import { LpCategoriesFilters } from '@/components/dashboard/lp_categories/lp-categories-filters';
|
import { LpCategoriesFilters } from '@/components/dashboard/lp/categories/lp-categories-filters';
|
||||||
import type { Filters } from '@/components/dashboard/lp_categories/lp-categories-filters';
|
import type { Filters } from '@/components/dashboard/lp/categories/lp-categories-filters';
|
||||||
import { LpCategoriesPagination } from '@/components/dashboard/lp_categories/lp-categories-pagination';
|
import { LpCategoriesPagination } from '@/components/dashboard/lp/categories/lp-categories-pagination';
|
||||||
import { LpCategoriesSelectionProvider } from '@/components/dashboard/lp_categories/lp-categories-selection-context';
|
import { LpCategoriesSelectionProvider } from '@/components/dashboard/lp/categories/lp-categories-selection-context';
|
||||||
import { LpCategoriesTable } from '@/components/dashboard/lp_categories/lp-categories-table';
|
import { LpCategoriesTable } from '@/components/dashboard/lp/categories/lp-categories-table';
|
||||||
import type { LpCategory } from '@/components/dashboard/lp_categories/type';
|
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||||
import FormLoading from '@/components/loading';
|
import FormLoading from '@/components/loading';
|
||||||
|
|
||||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||||
const { t } = useTranslation(['lp_categories']);
|
const { t } = useTranslation(['lp_categories']);
|
||||||
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||||
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<LpCategory[]>([]);
|
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<LpCategory[]>([]);
|
||||||
//
|
//
|
||||||
|
|
||||||
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
||||||
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||||
//
|
//
|
||||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||||
|
//
|
||||||
const [f, setF] = React.useState<LpCategory[]>([]);
|
const [f, setF] = React.useState<LpCategory[]>([]);
|
||||||
const [currentPage, setCurrentPage] = React.useState<number>(1);
|
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||||
|
//
|
||||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||||
const [listOption, setListOption] = React.useState({});
|
const [listOption, setListOption] = React.useState({});
|
||||||
const [listSort, setListSort] = React.useState({});
|
const [listSort, setListSort] = React.useState({});
|
||||||
@@ -54,6 +55,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
|
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
|
||||||
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status });
|
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status });
|
||||||
|
|
||||||
|
//
|
||||||
const reloadRows = async (): Promise<void> => {
|
const reloadRows = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const models: ListResult<RecordModel> = await pb
|
const models: ListResult<RecordModel> = await pb
|
||||||
@@ -67,26 +69,81 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
setLessonCategoriesData(tempLessonTypes);
|
setLessonCategoriesData(tempLessonTypes);
|
||||||
setRecordCount(totalItems);
|
setRecordCount(totalItems);
|
||||||
setF(tempLessonTypes);
|
setF(tempLessonTypes);
|
||||||
console.log({ currentPage, f });
|
// console.log({ currentPage, f });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//
|
//
|
||||||
setShowError({ show: true, detail: JSON.stringify(error) });
|
logger.error(error);
|
||||||
|
setShowError({
|
||||||
|
//
|
||||||
|
show: true,
|
||||||
|
detail: JSON.stringify(error, null, 2),
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setShowLoading(false);
|
setShowLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [lastListOption, setLastListOption] = React.useState({});
|
||||||
|
const isFirstRun = React.useRef(false);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
void reloadRows();
|
if (!isFirstRun.current) {
|
||||||
|
isFirstRun.current = true;
|
||||||
|
} else {
|
||||||
|
if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
||||||
|
// reset page number as tab changes
|
||||||
|
setLastListOption(listOption);
|
||||||
|
setCurrentPage(0);
|
||||||
|
void reloadRows();
|
||||||
|
} else {
|
||||||
|
void reloadRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
}, [currentPage, rowsPerPage, listOption]);
|
}, [currentPage, rowsPerPage, listOption]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let tempFilter = [],
|
||||||
|
tempSortDir = '';
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
tempFilter.push(`visible = "${visible}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDir) {
|
||||||
|
tempSortDir = `-created`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
tempFilter.push(`name ~ "%${name}%"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
tempFilter.push(`type ~ "%${type}%"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let preFinalListOption = {};
|
||||||
|
if (tempFilter.length > 0) {
|
||||||
|
preFinalListOption = { filter: tempFilter.join(' && ') };
|
||||||
|
}
|
||||||
|
if (tempSortDir.length > 0) {
|
||||||
|
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
|
||||||
|
}
|
||||||
|
setListOption(preFinalListOption);
|
||||||
|
// setListOption({
|
||||||
|
// filter: tempFilter.join(' && '),
|
||||||
|
// sort: tempSortDir,
|
||||||
|
// //
|
||||||
|
// });
|
||||||
|
}, [visible, sortDir, name, type]);
|
||||||
|
|
||||||
|
// return <>helloworld</>;
|
||||||
|
|
||||||
if (showLoading) return <FormLoading />;
|
if (showLoading) return <FormLoading />;
|
||||||
|
|
||||||
if (showError.show)
|
if (showError.show)
|
||||||
return (
|
return (
|
||||||
<ErrorDisplay
|
<ErrorDisplay
|
||||||
message={t('error.unable-to-process-request')}
|
message={t('error.unable-to-process-request')}
|
||||||
code="500"
|
code={-1}
|
||||||
details={showError.detail}
|
details={showError.detail}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import { PropertyItem } from '@/components/core/property-item';
|
import { PropertyItem } from '@/components/core/property-item';
|
||||||
import { PropertyList } from '@/components/core/property-list';
|
import { PropertyList } from '@/components/core/property-list';
|
||||||
import { LpCategory } from '@/components/dashboard/lp_categories/type';
|
import { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||||
|
|
||||||
export default function BasicDetailCard({
|
export default function BasicDetailCard({
|
||||||
lpModel: model,
|
lpModel: model,
|
||||||
|
@@ -10,7 +10,7 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
|
|||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { LpCategory } from '@/components/dashboard/lp_categories/type';
|
import { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||||
|
|
||||||
function getImageUrlFrRecord(record: LpCategory): string {
|
function getImageUrlFrRecord(record: LpCategory): string {
|
||||||
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
||||||
|
@@ -5,8 +5,8 @@ import RouterLink from 'next/link';
|
|||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
|
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
|
||||||
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
|
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
|
||||||
import SamplePaymentCard from '@/app/dashboard/Sample/SamplePaymentCard';
|
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
|
||||||
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
|
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
|
||||||
import { COL_QUIZ_LP_QUESTIONS } from '@/constants';
|
import { COL_QUIZ_LP_QUESTIONS } from '@/constants';
|
||||||
import { Grid } from '@mui/material';
|
import { Grid } from '@mui/material';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
@@ -21,9 +21,9 @@ import { logger } from '@/lib/default-logger';
|
|||||||
import { pb } from '@/lib/pb';
|
import { pb } from '@/lib/pb';
|
||||||
import { toast } from '@/components/core/toaster';
|
import { toast } from '@/components/core/toaster';
|
||||||
import ErrorDisplay from '@/components/dashboard/error';
|
import ErrorDisplay from '@/components/dashboard/error';
|
||||||
import { defaultLpQuestion } from '@/components/dashboard/lp_questions/_constants.ts';
|
import { defaultLpQuestion } from '@/components/dashboard/lp/questions/_constants.ts';
|
||||||
import { Notifications } from '@/components/dashboard/lp_questions/notifications';
|
import { Notifications } from '@/components/dashboard/lp/questions/notifications';
|
||||||
import type { LpQuestion } from '@/components/dashboard/lp_questions/type';
|
import type { LpQuestion } from '@/components/dashboard/lp/questions/type';
|
||||||
import FormLoading from '@/components/loading';
|
import FormLoading from '@/components/loading';
|
||||||
|
|
||||||
import BasicDetailCard from './BasicDetailCard';
|
import BasicDetailCard from './BasicDetailCard';
|
||||||
|
@@ -13,7 +13,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
import { LpQuestionCreateForm } from '@/components/dashboard/lp_questions/lp-question-create-form';
|
import { LpQuestionCreateForm } from '@/components/dashboard/lp/questions/lp-question-create-form';
|
||||||
|
|
||||||
export default function Page(): React.JSX.Element {
|
export default function Page(): React.JSX.Element {
|
||||||
// RULES: follow the name of page directory
|
// RULES: follow the name of page directory
|
||||||
|
@@ -10,7 +10,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
import { LpQuestionEditForm } from '@/components/dashboard/lp_questions/lp-question-edit-form';
|
import { LpQuestionEditForm } from '@/components/dashboard/lp/questions/lp-question-edit-form';
|
||||||
|
|
||||||
export default function Page(): React.JSX.Element {
|
export default function Page(): React.JSX.Element {
|
||||||
const { t } = useTranslation(['lp_questions']);
|
const { t } = useTranslation(['lp_questions']);
|
||||||
|
@@ -23,13 +23,13 @@ import { logger } from '@/lib/default-logger';
|
|||||||
import { pb } from '@/lib/pb';
|
import { pb } from '@/lib/pb';
|
||||||
import { toast } from '@/components/core/toaster';
|
import { toast } from '@/components/core/toaster';
|
||||||
import ErrorDisplay from '@/components/dashboard/error';
|
import ErrorDisplay from '@/components/dashboard/error';
|
||||||
import { defaultLpQuestion } from '@/components/dashboard/lp_questions/_constants';
|
import { defaultLpQuestion } from '@/components/dashboard/lp/questions/_constants';
|
||||||
import { LpQuestionsFilters } from '@/components/dashboard/lp_questions/lp-questions-filters';
|
import { LpQuestionsFilters } from '@/components/dashboard/lp/questions/lp-questions-filters';
|
||||||
import type { Filters } from '@/components/dashboard/lp_questions/lp-questions-filters';
|
import type { Filters } from '@/components/dashboard/lp/questions/lp-questions-filters';
|
||||||
import { LpQuestionsPagination } from '@/components/dashboard/lp_questions/lp-questions-pagination';
|
import { LpQuestionsPagination } from '@/components/dashboard/lp/questions/lp-questions-pagination';
|
||||||
import { LpQuestionsSelectionProvider } from '@/components/dashboard/lp_questions/lp-questions-selection-context';
|
import { LpQuestionsSelectionProvider } from '@/components/dashboard/lp/questions/lp-questions-selection-context';
|
||||||
import { LpQuestionsTable } from '@/components/dashboard/lp_questions/lp-questions-table';
|
import { LpQuestionsTable } from '@/components/dashboard/lp/questions/lp-questions-table';
|
||||||
import type { LpQuestion } from '@/components/dashboard/lp_questions/type';
|
import type { LpQuestion } from '@/components/dashboard/lp/questions/type';
|
||||||
import FormLoading from '@/components/loading';
|
import FormLoading from '@/components/loading';
|
||||||
|
|
||||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||||
@@ -44,8 +44,10 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||||
//
|
//
|
||||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||||
|
//
|
||||||
const [f, setF] = React.useState<LpQuestion[]>([]);
|
const [f, setF] = React.useState<LpQuestion[]>([]);
|
||||||
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||||
|
//
|
||||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||||
const [listOption, setListOption] = React.useState({});
|
const [listOption, setListOption] = React.useState({});
|
||||||
const [listSort, setListSort] = React.useState({});
|
const [listSort, setListSort] = React.useState({});
|
||||||
|
1787
002_source/cms/src/app/dashboard/lp/repomix-output.xml
Normal file
1787
002_source/cms/src/app/dashboard/lp/repomix-output.xml
Normal file
File diff suppressed because it is too large
Load Diff
3336
002_source/cms/src/app/dashboard/mf/_repomix.md
Normal file
3336
002_source/cms/src/app/dashboard/mf/_repomix.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,8 @@ import RouterLink from 'next/link';
|
|||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
|
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
|
||||||
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
|
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
|
||||||
import SamplePaymentCard from '@/app/dashboard/Sample/SamplePaymentCard';
|
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
|
||||||
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
|
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
|
||||||
import { COL_QUIZ_MF_CATEGORIES } from '@/constants';
|
import { COL_QUIZ_MF_CATEGORIES } from '@/constants';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
|
@@ -13,7 +13,7 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
import { MfCategoryCreateForm } from '@/components/dashboard/mf/categories/lp-category-create-form';
|
import { MfCategoryCreateForm } from '@/components/dashboard/mf/categories/mf-category-create-form';
|
||||||
|
|
||||||
export default function Page(): React.JSX.Element {
|
export default function Page(): React.JSX.Element {
|
||||||
// RULES: follow the name of page directory
|
// RULES: follow the name of page directory
|
||||||
|
@@ -33,8 +33,7 @@ import type { MfCategory } from '@/components/dashboard/mf/categories/type';
|
|||||||
import FormLoading from '@/components/loading';
|
import FormLoading from '@/components/loading';
|
||||||
|
|
||||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||||
// TODO: modify from lp_categories to mf_categories
|
const { t } = useTranslation(['mf_categories']);
|
||||||
const { t } = useTranslation(['lp_categories']);
|
|
||||||
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<MfCategory[]>([]);
|
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<MfCategory[]>([]);
|
||||||
@@ -59,7 +58,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
try {
|
try {
|
||||||
const models: ListResult<RecordModel> = await pb
|
const models: ListResult<RecordModel> = await pb
|
||||||
.collection(COL_QUIZ_MF_CATEGORIES)
|
.collection(COL_QUIZ_MF_CATEGORIES)
|
||||||
.getList(currentPage + 1, rowsPerPage, {});
|
.getList(currentPage + 1, rowsPerPage, listOption);
|
||||||
const { items, totalItems } = models;
|
const { items, totalItems } = models;
|
||||||
const tempLessonTypes: MfCategory[] = items.map((lt) => {
|
const tempLessonTypes: MfCategory[] = items.map((lt) => {
|
||||||
return { ...defaultMfCategory, ...lt };
|
return { ...defaultMfCategory, ...lt };
|
||||||
@@ -68,26 +67,81 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
setLessonCategoriesData(tempLessonTypes);
|
setLessonCategoriesData(tempLessonTypes);
|
||||||
setRecordCount(totalItems);
|
setRecordCount(totalItems);
|
||||||
setF(tempLessonTypes);
|
setF(tempLessonTypes);
|
||||||
console.log({ currentPage, f });
|
// console.log({ currentPage, f });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//
|
//
|
||||||
setShowError({ show: true, detail: JSON.stringify(error) });
|
logger.error(error);
|
||||||
|
setShowError({
|
||||||
|
//
|
||||||
|
show: true,
|
||||||
|
detail: JSON.stringify(error, null, 2),
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setShowLoading(false);
|
setShowLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [lastListOption, setLastListOption] = React.useState({});
|
||||||
|
const isFirstRun = React.useRef(false);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
void reloadRows();
|
if (!isFirstRun.current) {
|
||||||
|
isFirstRun.current = true;
|
||||||
|
} else {
|
||||||
|
if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
||||||
|
// reset page number as tab changes
|
||||||
|
setLastListOption(listOption);
|
||||||
|
setCurrentPage(0);
|
||||||
|
void reloadRows();
|
||||||
|
} else {
|
||||||
|
void reloadRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
}, [currentPage, rowsPerPage, listOption]);
|
}, [currentPage, rowsPerPage, listOption]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let tempFilter = [],
|
||||||
|
tempSortDir = '';
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
tempFilter.push(`visible = "${visible}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDir) {
|
||||||
|
tempSortDir = `-created`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
tempFilter.push(`name ~ "%${name}%"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
tempFilter.push(`type ~ "%${type}%"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let preFinalListOption = {};
|
||||||
|
if (tempFilter.length > 0) {
|
||||||
|
preFinalListOption = { filter: tempFilter.join(' && ') };
|
||||||
|
}
|
||||||
|
if (tempSortDir.length > 0) {
|
||||||
|
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
|
||||||
|
}
|
||||||
|
setListOption(preFinalListOption);
|
||||||
|
// setListOption({
|
||||||
|
// filter: tempFilter.join(' && '),
|
||||||
|
// sort: tempSortDir,
|
||||||
|
// //
|
||||||
|
// });
|
||||||
|
}, [visible, sortDir, name, type]);
|
||||||
|
|
||||||
|
// return <>helloworld</>;
|
||||||
|
|
||||||
if (showLoading) return <FormLoading />;
|
if (showLoading) return <FormLoading />;
|
||||||
|
|
||||||
if (showError.show)
|
if (showError.show)
|
||||||
return (
|
return (
|
||||||
<ErrorDisplay
|
<ErrorDisplay
|
||||||
message={t('error.unable-to-process-request')}
|
message={t('error.unable-to-process-request')}
|
||||||
code="500"
|
code={-1}
|
||||||
details={showError.detail}
|
details={showError.detail}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@@ -5,8 +5,8 @@ import RouterLink from 'next/link';
|
|||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
|
import SampleAddressCard from '@/app/dashboard/Sample/AddressCard';
|
||||||
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
|
import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
|
||||||
import SamplePaymentCard from '@/app/dashboard/Sample/SamplePaymentCard';
|
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
|
||||||
import SampleSecurityCard from '@/app/dashboard/Sample/SampleSecurityCard';
|
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
|
||||||
import { COL_QUIZ_MF_QUESTIONS } from '@/constants';
|
import { COL_QUIZ_MF_QUESTIONS } from '@/constants';
|
||||||
import { Grid } from '@mui/material';
|
import { Grid } from '@mui/material';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
@@ -33,7 +33,7 @@ import type { MfQuestion } from '@/components/dashboard/mf/questions/type';
|
|||||||
import FormLoading from '@/components/loading';
|
import FormLoading from '@/components/loading';
|
||||||
|
|
||||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||||
const { t } = useTranslation(['lp_questions']);
|
const { t } = useTranslation(['mf_question']);
|
||||||
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [lessonQuestionsData, setLessonCategoriesData] = React.useState<MfQuestion[]>([]);
|
const [lessonQuestionsData, setLessonCategoriesData] = React.useState<MfQuestion[]>([]);
|
||||||
|
1663
002_source/cms/src/app/dashboard/mf/repomix-output.xml
Normal file
1663
002_source/cms/src/app/dashboard/mf/repomix-output.xml
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user