Compare commits
47 Commits
develop/cm
...
367e58a8cf
Author | SHA1 | Date | |
---|---|---|---|
![]() |
367e58a8cf | ||
![]() |
99274b9c73 | ||
![]() |
51935d203a | ||
![]() |
9d3e832081 | ||
![]() |
4c72861eda | ||
![]() |
d0ea7e5452 | ||
![]() |
7d70b5826b | ||
![]() |
a00d1ee7ce | ||
![]() |
7296a10ec1 | ||
![]() |
957df690f4 | ||
![]() |
45d5c23512 | ||
![]() |
caa224cbb6 | ||
![]() |
9be92b41d1 | ||
![]() |
6e8fea3bdd | ||
![]() |
df87cfb037 | ||
![]() |
d73e5f9c22 | ||
![]() |
6884f1466f | ||
![]() |
d308131a8a | ||
![]() |
b3ebe8309a | ||
![]() |
fa35ef2bef | ||
![]() |
0785fcd144 | ||
![]() |
2dcc765072 | ||
![]() |
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 |
@@ -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
|
||||
// lesson_types, lesson_type
|
||||
Table LessonTypes {
|
||||
// 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
|
||||
updated datetime // timestamp when the lesson type was last updated
|
||||
// value field
|
||||
name varchar // name of the lesson type
|
||||
type varchar // type category
|
||||
name text // changed from varchar to text
|
||||
type text // changed from varchar to text
|
||||
pos integer
|
||||
visible text
|
||||
field date
|
||||
}
|
||||
|
||||
|
||||
// LessonCategories stores categories of lessons
|
||||
// lesson_categories, lesson_category
|
||||
Table LessonCategories {
|
||||
// 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
|
||||
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
|
||||
cat_name text // changed from varchar to text
|
||||
cat_image_url text // new field
|
||||
cat_image file // changed from varchar to file
|
||||
pos integer
|
||||
lesson_id integer [ref: > LessonTypes.id] // foreign key referencing LessonTypes.id
|
||||
description text // new field
|
||||
remarks text // changed from varchar to text
|
||||
visible text // new field
|
||||
}
|
||||
|
||||
Table Helloworlds {
|
||||
// system field
|
||||
id int [pk, increment] // id field, increment
|
||||
id text [pk] // changed from int to text
|
||||
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
|
||||
hello text // new field
|
||||
}
|
||||
|
||||
Table UserMetas {
|
||||
// system field
|
||||
id int [pk, increment]
|
||||
id text [pk] // changed from int to text
|
||||
created datetime [default: `now()`]
|
||||
updated datetime
|
||||
|
||||
// value field
|
||||
helloworld varchar
|
||||
app_on_time_s integer
|
||||
user_id integer [ref: > Users.id]
|
||||
helloworld text // changed from varchar to text
|
||||
meta json // new field
|
||||
user_id text [ref: > Users.id] // changed type and reference
|
||||
state text // new field
|
||||
avatar file // changed from blob to file
|
||||
role text // new field
|
||||
}
|
||||
|
||||
Table QuizCategories {
|
||||
// system field
|
||||
id int [pk, increment]
|
||||
id text [pk] // changed from int to text
|
||||
created datetime [default: `now()`]
|
||||
updated datetime
|
||||
|
||||
// value field
|
||||
cat_name varchar // category name
|
||||
cat_image varchar // category image
|
||||
cat_name text // changed from varchar to text
|
||||
cat_image text // changed from varchar to text
|
||||
init_answer json // new field
|
||||
}
|
||||
|
||||
|
||||
// stores all questions of matching frenzy
|
||||
Table QuizMatchings {
|
||||
// system field
|
||||
id int [pk, increment] // id field, increment
|
||||
id text [pk] // changed from int to text
|
||||
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
|
||||
word text // changed from varchar to text
|
||||
word_c text // changed from varchar to text
|
||||
cat_id text [ref: > QuizCategories.id] // changed type and reference
|
||||
}
|
||||
|
||||
// QuizListening stores all listening quiz data
|
||||
Table QuizListenings {
|
||||
// system field
|
||||
id int [pk, increment] // id field, increment
|
||||
id text [pk] // changed from int to text
|
||||
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]
|
||||
sound file // changed from varchar to file
|
||||
word text // changed from varchar to text
|
||||
cat_id text [ref: > QuizCategories.id] // changed type and reference
|
||||
}
|
||||
|
||||
|
||||
// stores all categories of connectives revision quiz
|
||||
Table QuizConnectivesCategories {
|
||||
// system field
|
||||
id int [pk, increment] // id field, increment
|
||||
id text [pk] // changed from int to text
|
||||
created datetime [default: `now()`] // record create time
|
||||
updated datetime // record update time
|
||||
|
||||
// value field
|
||||
cat_name varchar // category name
|
||||
cat_image varchar // category image
|
||||
cat_name text // changed from varchar to text
|
||||
cat_image file // changed from varchar to file
|
||||
}
|
||||
|
||||
// stores all questions of connectives revision quiz
|
||||
Table QuizConnectives {
|
||||
// system field
|
||||
id int [pk, increment] // id field, increment
|
||||
id text [pk] // changed from int to text
|
||||
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
|
||||
question_fh text // changed from varchar to text
|
||||
question_sh text // changed from varchar to text
|
||||
modal_ans text // changed from varchar to text
|
||||
cat_id text [ref: > QuizConnectivesCategories.id] // changed type and reference
|
||||
}
|
||||
|
||||
// Lessons stores all lessons in the database
|
||||
Table Vocabularies {
|
||||
// system field
|
||||
id 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
|
||||
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
|
||||
image file // changed from varchar to file
|
||||
sound file // changed from varchar to file
|
||||
word text // changed from varchar to text
|
||||
word_c text // changed from varchar to text
|
||||
sample_e text // changed from varchar to text
|
||||
sample_c text // changed from varchar to text
|
||||
cat_id text [ref: > LessonCategories.id] // changed type and reference
|
||||
category text // changed from varchar to text
|
||||
lesson_type_id text [ref: > LessonTypes.id] // changed type and reference
|
||||
}
|
||||
|
||||
// Listening Practice Quiz Categories
|
||||
// store listening practice category, (LpCategories, LpCategory)
|
||||
Table QuizLPCategories {
|
||||
// system fields
|
||||
id text [pk] // changed from int to text to match PocketBase
|
||||
id text [pk]
|
||||
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
|
||||
cat_name text
|
||||
cat_image file
|
||||
pos number
|
||||
init_answer json
|
||||
visible text
|
||||
slug text
|
||||
remarks text
|
||||
description text
|
||||
}
|
||||
|
||||
// Listening Practice Quiz Questions
|
||||
Table QuizLPQuestions {
|
||||
id int [pk, increment]
|
||||
id text [pk] // changed from int to text
|
||||
created datetime [default: `now()`]
|
||||
updated datetime
|
||||
word varchar
|
||||
sound blob
|
||||
cat_id integer [ref: > QuizLPCategories.id]
|
||||
word text // changed from varchar to text
|
||||
sound file // changed from blob to file
|
||||
cat_id text [ref: > QuizLPCategories.id] // changed type and reference
|
||||
cat_name text // new field
|
||||
cat_image file // new field
|
||||
pos number // new field
|
||||
init_answer json // new field
|
||||
visible text // new field
|
||||
slug text // new field
|
||||
remarks text // new field
|
||||
description text // new field
|
||||
}
|
||||
|
||||
// Matching Frenzy Quiz Categories
|
||||
Table QuizMFCategories {
|
||||
id int [pk, increment]
|
||||
id text [pk] // changed from int to text
|
||||
created datetime [default: `now()`]
|
||||
updated datetime
|
||||
cat_name varchar
|
||||
cat_image blob
|
||||
pos integer
|
||||
cat_name text // changed from varchar to text
|
||||
cat_image file // changed from blob to file
|
||||
pos number // changed from integer to number
|
||||
init_answer json
|
||||
visible text // new field
|
||||
}
|
||||
|
||||
// Matching Frenzy Quiz Questions
|
||||
Table QuizMFQuestions {
|
||||
id int [pk, increment]
|
||||
id text [pk] // changed from int to text
|
||||
created datetime [default: `now()`]
|
||||
updated datetime
|
||||
word varchar
|
||||
word_c varchar
|
||||
cat_id integer [ref: > QuizMFCategories.id]
|
||||
word text // changed from varchar to text
|
||||
word_c text // changed from varchar to text
|
||||
cat_id text [ref: > QuizMFCategories.id] // changed type and reference
|
||||
visible text // new field
|
||||
sound file // new field
|
||||
cat_image file // new field
|
||||
}
|
||||
|
||||
// Connectives Revision Quiz Categories
|
||||
Table QuizCRCategories {
|
||||
id int [pk, increment]
|
||||
id text [pk] // changed from int to text
|
||||
created datetime [default: `now()`]
|
||||
updated datetime
|
||||
cat_name varchar
|
||||
cat_image blob
|
||||
cat_name text // changed from varchar to text
|
||||
cat_image file // changed from blob to file
|
||||
pos integer
|
||||
init_answer json
|
||||
}
|
||||
|
||||
// Connectives Revision Quiz Questions
|
||||
Table QuizCRQuestions {
|
||||
id int [pk, increment]
|
||||
id text [pk] // changed from int to text
|
||||
created datetime [default: `now()`]
|
||||
updated datetime
|
||||
question_fh varchar
|
||||
question_sh varchar
|
||||
modal_ans varchar
|
||||
cat_id integer [ref: > QuizCRCategories.id]
|
||||
question_fh text // changed from varchar to text
|
||||
question_sh text // changed from varchar to text
|
||||
modal_ans text // changed from varchar to text
|
||||
cat_id text [ref: > QuizCRCategories.id] // changed type and reference
|
||||
options json // new field
|
||||
}
|
||||
|
||||
// Test table
|
||||
Table t1 {
|
||||
id int [pk, increment]
|
||||
id text [pk] // changed from int to text
|
||||
created datetime [default: `now()`]
|
||||
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 project = resolve(__dirname, 'tsconfig.json');
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
@@ -84,4 +83,13 @@ module.exports = {
|
||||
'react/jsx-sort-props': 'off',
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
33
002_source/cms/.vscode/extensions.json
vendored
Normal file
33
002_source/cms/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"wmaurer.change-case",
|
||||
"saoudrizwan.claude-dev",
|
||||
"naumovs.color-highlight",
|
||||
"bocovo.dbml-erd-visualizer",
|
||||
"aflalo.dbml-formatter",
|
||||
"editorconfig.editorconfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-vscode.vscode-typescript-next",
|
||||
"tttpob.list-aligner",
|
||||
"bierner.markdown-mermaid",
|
||||
"onatm.open-in-new-window",
|
||||
"christian-kohler.path-intellisense",
|
||||
"esbenp.prettier-vscode",
|
||||
"humao.rest-client",
|
||||
"ryu1kn.text-marker",
|
||||
"gruntfuggly.todo-tree",
|
||||
"bibhasdn.unique-lines",
|
||||
"neptunedesign.vs-sequential-number",
|
||||
"matt-meyers.vscode-dbml",
|
||||
"codeium.codeium",
|
||||
"tencent-cloud.coding-copilot",
|
||||
"yzhang.markdown-all-in-one",
|
||||
"stubedston.align-text",
|
||||
"mads-hartmann.bash-ide-vscode",
|
||||
"duynvu.dbml-language",
|
||||
"mikestead.dotenv",
|
||||
"bpruitt-goddard.mermaid-markdown-syntax-highlighting",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"foxundermoon.shell-format"
|
||||
]
|
||||
}
|
@@ -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
|
20
002_source/cms/_AI_WORKSPACE/code/002_update_app_page.md
Normal file
20
002_source/cms/_AI_WORKSPACE/code/002_update_app_page.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# task
|
||||
|
||||
update app page to cover `vocabulary`
|
||||
|
||||
## steps
|
||||
|
||||
1. read `tsx` from folder: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/vocabularies.draft`
|
||||
1. fix the paths, variable, function names, class etc from `lesson-categories` to `vocabularies`.
|
||||
|
||||
thanks
|
||||
|
||||
## FAQ
|
||||
|
||||
1. 在 `constants.ts` 中是否已定义 `COL_VOCABULARIES` 常量?没有,需要先定义它。
|
||||
2. Vocabulary 相关的类型定义在哪里?是在现有文件中还是需要新建? 需要新建
|
||||
3. 是否需要保留原始 `lessonCategories` 驱动文件? 不需要
|
||||
|
||||
- `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/vocabularies.draft`
|
||||
|
||||
- `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/vocabulary.draft`
|
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
|
@@ -0,0 +1,13 @@
|
||||
---
|
||||
tags: update-constants-file
|
||||
---
|
||||
|
||||
# task
|
||||
|
||||
update constants file
|
||||
|
||||
## steps
|
||||
|
||||
- have a look to `_constants.ts` files in slibing directory
|
||||
- get the convention
|
||||
- update `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/vocabulary/_constants.ts` according to dbml thanks
|
@@ -0,0 +1,13 @@
|
||||
---
|
||||
tags: review-function-names
|
||||
---
|
||||
|
||||
# task
|
||||
|
||||
review function names
|
||||
|
||||
## steps
|
||||
|
||||
- have a look to `tsx` files in slibing directory `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Vocabularies`
|
||||
- get the convention
|
||||
- update the function name of `tsx` file in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Vocabularies`
|
49
002_source/cms/_AI_WORKSPACE/db/001_INIT.md
Normal file
49
002_source/cms/_AI_WORKSPACE/db/001_INIT.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# AI GUIDELINE
|
||||
|
||||
## getting started
|
||||
|
||||
Imagine there is a:
|
||||
|
||||
1. developer (provide the modification)
|
||||
2. QA engineer (provide the feedback, and testing)
|
||||
3. software engineer
|
||||
4. technical writer
|
||||
|
||||
they will:
|
||||
|
||||
- conclude and integrate the ideas from developer and QA engineer
|
||||
- make decision to modify the code accordingly.
|
||||
|
||||
## project background and initial setup
|
||||
|
||||
- **IMPORTANT**: No need to reply me what you are going on and your digest in this phase.
|
||||
No need to show me your code plan
|
||||
Just reply me "OK" when done
|
||||
|
||||
- base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project`
|
||||
|
||||
- `schema.dbml`
|
||||
|
||||
- read `<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`
|
||||
|
||||
- if the directory user provided contins `_GUIDELINES.md`, please read the file
|
||||
|
||||
- read the files, remember and link up the ideas in file stated above, i will tell them the task afterwards
|
||||
|
||||
- please review at least 3 times after you modified the code
|
||||
|
||||
## frameworks documentation and samples
|
||||
|
||||
- `MUI`
|
||||
- `<base_dir>/002_source/cms/src/components/widgets/forms` contains sample forms,
|
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,27 @@
|
||||
Hi, i need your help.
|
||||
|
||||
## task
|
||||
|
||||
i am working on a `dbml` file
|
||||
i got a `schema.json` which is exported from pocketbase
|
||||
and i want to update it to my current `dbml` file (one way process for documentation usage)
|
||||
|
||||
## Rules
|
||||
|
||||
- the collection from `json` file started with `_` can be ignored. they are system collection and should not appear in `dbml`
|
||||
- one collection from `json` file mapped with one table in `dbml` file
|
||||
- the `presentable` field from `json` file should be ignored.
|
||||
- the `id` of collection in `json` file should be jod down in the comment of `dbml` file as an reference.
|
||||
|
||||
## 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`
|
||||
|
||||
## FAQ
|
||||
|
||||
1. 对于json中有但dbml中没有的表,应该如何处理? 添加为新表
|
||||
1. 是否需要保留dbml文件中现有的注释和关系定义? 完全保留
|
||||
1. 字段类型映射是否有特殊规则? 沒有
|
||||
|
||||
thanks
|
37
002_source/cms/_AI_WORKSPACE/db/004_clone_db_driver.md
Normal file
37
002_source/cms/_AI_WORKSPACE/db/004_clone_db_driver.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
tags: db, driver
|
||||
---
|
||||
|
||||
# clone db driver
|
||||
|
||||
please understand the tsx files in folder
|
||||
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/vocabularies.draft`
|
||||
|
||||
change all occurrence of `lessonCategories` to `vocabularies` thanks.
|
||||
|
||||
## FAQ
|
||||
|
||||
1. 在 `constants.ts` 中是否已定义 `COL_VOCABULARIES` 常量?没有,需要先定义它。
|
||||
2. Vocabulary 相关的类型定义在哪里?是在现有文件中还是需要新建? 需要新建
|
||||
3. 是否需要保留原始 `lessonCategories` 驱动文件? 不需要
|
||||
|
||||
请确认这些信息,以便我制定完整的修改方案。
|
||||
|
||||
当前已确认需要修改的内容包括:
|
||||
|
||||
函数名中的customer → notification
|
||||
COL_CUSTOMERS → COL_NOTIFICATIONS
|
||||
相关类型引用
|
||||
注释中的customer → notification
|
||||
|
||||
---
|
||||
|
||||
<!-- updat type.d -->
|
||||
|
||||
`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Notifications/type.d.ts` the fields is currently wrong, please help to update thanks.
|
||||
|
||||
update the `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Notifications/EmptyNotification.ts` as well thanks.
|
||||
|
||||
---
|
||||
|
||||
please draft `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Notifications/GetNotificationByUserId.tsx` by reference `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Notifications/GetById.tsx`
|
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,
|
||||
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`
|
0
002_source/cms/autogen_config.py
Normal file
0
002_source/cms/autogen_config.py
Normal file
@@ -22,8 +22,8 @@
|
||||
"test": "jest --watch",
|
||||
"update_doc": "cd 001_documentation/Requirements && node update_req_index.js",
|
||||
"update_doc:w": "pnpx nodemon --ext md --exec \"pnpm run update_doc\"",
|
||||
"update_repomix": "npx repomix",
|
||||
"update_repomix:w": "pnpx nodemon --delay 3 --exec \"pnpx repomix\"",
|
||||
"update_repomix": "node ./scripts/update_repomix.js",
|
||||
"update_repomix:w": "pnpx nodemon --delay 3 --exec \"npm run update_repomix\"",
|
||||
"clean": "rm -rf node_modules && rm -rf .next"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -91,6 +91,7 @@
|
||||
"zod": "3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^9.7.0",
|
||||
"@ianvs/prettier-plugin-sort-imports": "4.1.1",
|
||||
"@testing-library/jest-dom": "6.4.2",
|
||||
"@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
|
||||
version: 3.22.4
|
||||
devDependencies:
|
||||
'@faker-js/faker':
|
||||
specifier: ^9.7.0
|
||||
version: 9.7.0
|
||||
'@ianvs/prettier-plugin-sort-imports':
|
||||
specifier: 4.1.1
|
||||
version: 4.1.1(prettier@3.2.5)
|
||||
@@ -725,6 +728,10 @@ packages:
|
||||
resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==}
|
||||
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':
|
||||
resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -6303,6 +6310,8 @@ snapshots:
|
||||
|
||||
'@eslint/js@8.57.0': {}
|
||||
|
||||
'@faker-js/faker@9.7.0': {}
|
||||
|
||||
'@fastify/busboy@2.1.0': {}
|
||||
|
||||
'@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: [
|
||||
{
|
||||
files: ['*.tsx'],
|
||||
|
@@ -1,39 +0,0 @@
|
||||
# PROMPT
|
||||
|
||||
please update `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/public/locales/dev/translation.json`
|
||||
to add below translations, thanks.
|
||||
|
||||
```
|
||||
dashboard.lessonCategories.edit.title
|
||||
dashboard.lessonCategories.edit.avatar
|
||||
dashboard.lessonCategories.edit.avatarRequirements
|
||||
dashboard.lessonCategories.edit.name
|
||||
dashboard.lessonCategories.edit.type
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
please read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/components/dashboard/lesson_category/lesson-category-edit-form.tsx`
|
||||
and update `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/public/locales/dev/translation.json` the missing translations, thanks.
|
||||
|
||||
---
|
||||
|
||||
please refactor `common.json` to smaller translation files, thanks
|
||||
e.g. `lessonTypes` -> `lesson_type`
|
||||
|
||||
---
|
||||
|
||||
Hi, i want you to help merge two translation files.
|
||||
|
||||
base_dir=`/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/public/locales/dev`
|
||||
|
||||
I want you to merge the content
|
||||
|
||||
from `<base_dir>/lesson_type.json` (source file)
|
||||
to `<base_dir>/listening_practice.json` (dest file)
|
||||
|
||||
please extract , link up and remember the document properties
|
||||
(e.g. types, functions, variables, constants, etc)
|
||||
|
||||
update the variables and properties of dest file to reflect `listening practice categories`/`lp_categories`
|
@@ -1,135 +1 @@
|
||||
{
|
||||
"languageChanged": "語言已更改",
|
||||
"type": "類型",
|
||||
"helloworld": "你好,世界",
|
||||
"Add": "新增項目",
|
||||
"visible": "可視",
|
||||
"hidden": "屏閉",
|
||||
"Hidden": "屏閉",
|
||||
"Newest": "最新",
|
||||
"Oldest": "最舊",
|
||||
"Lesson position": "位置",
|
||||
"Lesson type": "種類",
|
||||
"Type": "種類",
|
||||
"Name": "名稱",
|
||||
"Filter by name": "依名稱過濾",
|
||||
"Filter by type": "依類型過濾",
|
||||
"Actions": "動作",
|
||||
"Created at": "建立時間",
|
||||
"Updated at": "更新時間",
|
||||
"Position": "位置",
|
||||
"Visible": "顯示",
|
||||
"Apply": "套用",
|
||||
"Overview": "概觀",
|
||||
"Dashboard": "儀表板",
|
||||
"Tickets": "票券",
|
||||
"Sign ups": "註冊",
|
||||
"Open issues": "開啟問題",
|
||||
"Closed issues": "關閉問題",
|
||||
"increase": "增加",
|
||||
"decrease": "減少",
|
||||
"common.loading": "載入中",
|
||||
"vs last month": "與上個月相比",
|
||||
"Dashboards": "儀表板",
|
||||
"App usage": "應用程式使用情況",
|
||||
"Find your dream job": "找到你的夢想工作",
|
||||
"Need help figuring things out?": "需要幫助嗎?",
|
||||
"Search for jobs that match your skills and apply to them directly": "搜索符合你技能的工作,直接申請",
|
||||
"Find answers to your questions and get in touch with our team.": "找到你的問題並與我們的團隊聯繫",
|
||||
"Explore documentation": "探索文件",
|
||||
"Learn how to get started with our product and make the most of it.": "學習如何開始使用我們的產品並充分利用它",
|
||||
"Search Jobs": "搜索工作",
|
||||
"Help Center": "幫助中心",
|
||||
"Documentation": "文件",
|
||||
"increase_in_app_usage_with": "使用者量增加",
|
||||
"new_products_purchased": "新產品購買",
|
||||
"Our subscriptions": "我們的訂閱",
|
||||
"this_year": "今年",
|
||||
"is_forecasted_to_increase_in_your_traffic_by_the_end_of_the_current_month": "今年預計增加你的流量",
|
||||
"Jan": "一月",
|
||||
"Feb": "二月",
|
||||
"Mar": "三月",
|
||||
"Apr": "四月",
|
||||
"May": "五月",
|
||||
"Jun": "六月",
|
||||
"Jul": "七月",
|
||||
"Aug": "八月",
|
||||
"Sep": "九月",
|
||||
"Oct": "十月",
|
||||
"Nov": "十一月",
|
||||
"Dec": "十二月",
|
||||
"See all subscriptions": "查看所有訂閱",
|
||||
"app_chat": "應用程式聊天",
|
||||
"Upcoming events": "即將發生的事件",
|
||||
"Based on the linked bank accounts": "基於連結的銀行帳戶",
|
||||
"App limits": "應用程式限制",
|
||||
"You have used {percentage} of your available spots.": "您已使用 {percentage} 的可用座位。升級計劃以建立更多專案",
|
||||
"userMessagesUnread": "hello userMessagesUnread",
|
||||
"Upgrade plan to create more projects": "升級計劃以建立更多專案",
|
||||
"You have almost reached your limit": "您已接近您的限制",
|
||||
"Upgrade plan": "升級計劃",
|
||||
"Help center": "幫助中心",
|
||||
"Jobs": "工作",
|
||||
"go_to_chat": "前往聊天",
|
||||
"See all events": "查看所有事件",
|
||||
"You have used": "您已使用",
|
||||
"of your available spots": "的可用座位",
|
||||
"Paid": "已付款",
|
||||
"Canceled": "已取消",
|
||||
"Pending": "待付款",
|
||||
"Expiring": "已過期",
|
||||
"Refunded": "已退款",
|
||||
"Total": "總計",
|
||||
"Total paid": "已付款",
|
||||
"Total cancelled": "已取消",
|
||||
"Total pending": "待付款",
|
||||
"year": "年",
|
||||
"month": "月",
|
||||
"week": "週",
|
||||
"day": "日",
|
||||
"hour": "小時",
|
||||
"minute": "分鐘",
|
||||
"second": "秒",
|
||||
"ago": "前",
|
||||
"remaining": "剩下",
|
||||
"days": "天",
|
||||
"hours": "小時",
|
||||
"minutes": "分鐘",
|
||||
"seconds": "秒",
|
||||
"Clear filters": "清除篩選",
|
||||
"Type information": "輸入資訊",
|
||||
"Cancel": "取消",
|
||||
"Delete": "刪除",
|
||||
"All": "全部",
|
||||
"categories": "課程分類",
|
||||
"loading": "載入中",
|
||||
"Notifications": "通知",
|
||||
"Mark all as read": "標記所有為已讀",
|
||||
"There are no notifications": "沒有通知",
|
||||
"listenings": "聽講 (Listenings)",
|
||||
"question_category": "問題分類",
|
||||
"question_list": "問題",
|
||||
"matching_frenzy": "配對 (Matching Frenzy)",
|
||||
"connective_revision": "連接詞 (Connective Revision)",
|
||||
"settings": "設定",
|
||||
"students": "學生",
|
||||
"dashboard.lessonCategories.title": "課程分類",
|
||||
"dashboard.lessonCategorys.edit.name": "課程分類名稱",
|
||||
"dashboard.lessonCategorys.edit.position": "課程分類順序",
|
||||
"dashboard.lessonCategorys.edit.title": "編輯課程分類",
|
||||
"dashboard.lessonCategorys.edit.visibleToUser": "顯示給使用者",
|
||||
"dashboard.lessonCategorys.edit.visible": "顯示",
|
||||
"dashboard.lessonCategorys.edit.hidden": "隱藏",
|
||||
"dashboard.lessonCategorys.edit.cancelButton": "取消",
|
||||
"dashboard.lessonCategorys.edit.updateButton": "更新",
|
||||
"dashboard.lessonCategorys.list.title": "課堂種類",
|
||||
"word-count": "字數",
|
||||
"unable-to-process-request": "無法處理請求",
|
||||
"detailed-error-information": "詳細錯誤資訊",
|
||||
"error": {
|
||||
"unable-to-process-request": "無法處理您的請求",
|
||||
"detailed-error-information": "詳細錯誤資訊"
|
||||
},
|
||||
"name-is-required": "名稱為必填",
|
||||
"listening-practice": "聽講練習"
|
||||
}
|
||||
{}
|
||||
|
@@ -1,70 +0,0 @@
|
||||
{
|
||||
"add": "新增課程分類",
|
||||
"title": "課程分類",
|
||||
"basic_info": "基本資訊",
|
||||
"error": {
|
||||
"unable-to-process-request": "無法處理您的請求",
|
||||
"detailed-error-information": "詳細錯誤資訊"
|
||||
},
|
||||
"create": {
|
||||
"title": "建立課程類型",
|
||||
"createButton": "建立",
|
||||
"cancelButton": "取消",
|
||||
"message": "請輸入課程類型名稱",
|
||||
"success": "課程類型建立成功",
|
||||
"error": "課程類型建立失敗",
|
||||
"typeInformation": "輸入課程類型資訊",
|
||||
"avatar": "上傳課程類型圖片",
|
||||
"avatar_select": "選擇圖片",
|
||||
"avatarRequirements": "最小 400x400 像素,PNG 或 JPEG",
|
||||
"select": "請選擇",
|
||||
"name": "課程類型名稱",
|
||||
"position": "課程類型順序",
|
||||
"visibleToUser": "顯示給使用者",
|
||||
"detail-information": "詳細資訊",
|
||||
"description": "課程類型描述",
|
||||
"remarks": "備註"
|
||||
},
|
||||
"edit": {
|
||||
"avatar": "上傳圖片",
|
||||
"avatarRequirements": "最小 400x400 像素,PNG 或 JPEG",
|
||||
"cancelButton": "取消",
|
||||
"error": "課程類型更新失敗",
|
||||
"hidden": "隱藏",
|
||||
"name": "課程分類名稱",
|
||||
"position": "課程分類順序",
|
||||
"select": "請選擇",
|
||||
"success": "課程類型更新成功",
|
||||
"title": "編輯課程分類",
|
||||
"basic-info": "基本資訊",
|
||||
"type": "類型",
|
||||
"updateButton": "更新",
|
||||
"visible": "顯示",
|
||||
"visibleToUser": "顯示給使用者",
|
||||
"write-something": "輸入一些內容"
|
||||
},
|
||||
"delete": {
|
||||
"title": "刪除課程類型",
|
||||
"message": "確定要刪除課程類型嗎?",
|
||||
"success": "課程類型刪除成功",
|
||||
"error": "課程類型刪除失敗",
|
||||
"cancelButton": "取消",
|
||||
"deleteButton": "刪除"
|
||||
},
|
||||
"list": {
|
||||
"title": "課堂種類",
|
||||
"message": "請選擇課程類型",
|
||||
"empty": {
|
||||
"title": "目前沒有課程類型",
|
||||
"message": "請建立課程類型",
|
||||
"create": "建立課程類型"
|
||||
},
|
||||
"error": "課程類型載入失敗",
|
||||
"add": "新增課程分類"
|
||||
},
|
||||
"view": {
|
||||
"basic-details": "基本資訊"
|
||||
},
|
||||
"Delete Lesson Type ?": "刪除課程類型?",
|
||||
"Are you sure you want to delete lesson type ?": "確定要刪除課程類型嗎?"
|
||||
}
|
@@ -1,65 +0,0 @@
|
||||
{
|
||||
"add": "新增課程類型",
|
||||
"title": "課程類型",
|
||||
"type": "類型",
|
||||
"error": {
|
||||
"unable-to-process-request": "無法處理您的請求",
|
||||
"detailed-error-information": "詳細錯誤資訊"
|
||||
},
|
||||
"create": {
|
||||
"title": "建立課程類型",
|
||||
"createButton": "建立",
|
||||
"cancelButton": "取消",
|
||||
"message": "請輸入課程類型名稱",
|
||||
"success": "課程類型建立成功",
|
||||
"error": "課程類型建立失敗",
|
||||
"typeInformation": "輸入課程類型資訊",
|
||||
"avatar": "上傳課程類型圖片",
|
||||
"avatarRequirements": "上傳課程類型圖片",
|
||||
"select": "請選擇",
|
||||
"name": "課程類型名稱",
|
||||
"position": "課程類型順序",
|
||||
"visibleToUser": "顯示給使用者"
|
||||
},
|
||||
"edit": {
|
||||
"avatar": "上傳課程類型圖片",
|
||||
"avatarRequirements": "上傳課程類型圖片",
|
||||
"cancelButton": "取消",
|
||||
"error": "課程類型更新失敗",
|
||||
"hidden": "隱藏",
|
||||
"name": "課程類型名稱",
|
||||
"position": "課程類型順序",
|
||||
"select": "請選擇",
|
||||
"success": "課程類型更新成功",
|
||||
"title": "編輯課程類型",
|
||||
"typeInformation": "輸入課程類型資訊",
|
||||
"updateButton": "更新",
|
||||
"visible": "顯示",
|
||||
"visibleToUser": "顯示給使用者"
|
||||
},
|
||||
"delete": {
|
||||
"title": "刪除課程類型",
|
||||
"message": "確定要刪除課程類型嗎?",
|
||||
"success": "課程類型刪除成功",
|
||||
"error": "課程類型刪除失敗",
|
||||
"cancelButton": "取消",
|
||||
"deleteButton": "刪除"
|
||||
},
|
||||
"list": {
|
||||
"title": "課程類型列表",
|
||||
"message": "請選擇課程類型",
|
||||
"empty": {
|
||||
"title": "目前沒有課程類型",
|
||||
"message": "請建立課程類型",
|
||||
"create": "建立課程類型"
|
||||
},
|
||||
"error": "課程類型載入失敗"
|
||||
},
|
||||
"view": {
|
||||
"basic-details": "基本資訊"
|
||||
},
|
||||
"Lesson Type": "課程類型",
|
||||
"Lesson Types": "課程類型",
|
||||
"Delete Lesson Type ?": "刪除課程類型?",
|
||||
"Are you sure you want to delete lesson type ?": "確定要刪除課程類型嗎?"
|
||||
}
|
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"add": "新增",
|
||||
"listening-practice": "聽力練習",
|
||||
"list": {
|
||||
"title": "聽力練習分類列表",
|
||||
"empty": "沒有找到聽力練習分類",
|
||||
"action": "動作?",
|
||||
"basic-details": "基本資料"
|
||||
},
|
||||
"helloworld": "listening_practice",
|
||||
"title": "聽力練習分類",
|
||||
"type": "聽力練習分類",
|
||||
"error": {
|
||||
"invalid": "無效的聽力練習分類",
|
||||
"not_found": "未找到聽力練習分類"
|
||||
},
|
||||
"create": {
|
||||
"title": "創建聽力練習分類",
|
||||
"success": "聽力練習分類創建成功",
|
||||
"error": "創建聽力練習分類時出錯"
|
||||
},
|
||||
"edit": {
|
||||
"title": "編輯聽力練習分類",
|
||||
"success": "聽力練習分類更新成功",
|
||||
"error": "更新聽力練習分類時出錯"
|
||||
},
|
||||
"delete": {
|
||||
"title": "刪除聽力練習分類",
|
||||
"confirm": "確定要刪除聽力練習分類嗎?",
|
||||
"success": "聽力練習分類刪除成功",
|
||||
"error": "刪除聽力練習分類時出錯"
|
||||
},
|
||||
"view": {
|
||||
"title": "查看聽力練習分類詳情"
|
||||
}
|
||||
}
|
@@ -1,200 +0,0 @@
|
||||
{
|
||||
"languageChanged": "語言已更改",
|
||||
"type": "類型",
|
||||
"helloworld": "你好,世界",
|
||||
"Add": "新增項目",
|
||||
"visible": "可視",
|
||||
"hidden": "屏閉",
|
||||
"Hidden": "屏閉",
|
||||
"Newest": "最新",
|
||||
"Oldest": "最舊",
|
||||
"Lesson position": "位置",
|
||||
"Lesson type": "種類",
|
||||
"Type": "種類",
|
||||
"Name": "名稱",
|
||||
"Filter by name": "依名稱過濾",
|
||||
"Filter by type": "依類型過濾",
|
||||
"dashboard.lessonTypes.add": "新增課程類型",
|
||||
"dashboard.lessonTypes.title": "課程類型",
|
||||
"dashboard.lessonTypes.type": "類型",
|
||||
"dashboard.lessonTypes.create.title": "建立課程類型",
|
||||
"dashboard.lessonTypes.create.createButton": "建立",
|
||||
"dashboard.lessonTypes.create.cancelButton": "取消",
|
||||
"dashboard.lessonTypes.edit.title": "編輯課程類型",
|
||||
"Actions": "動作",
|
||||
"Created at": "建立時間",
|
||||
"Updated at": "更新時間",
|
||||
"dashboard.lessonTypes.edit": "編輯課程類型",
|
||||
"dashboard.lessonTypes.delete": "刪除課程類型",
|
||||
"dashboard.lessonTypes.delete.title": "刪除課程類型",
|
||||
"dashboard.lessonTypes.delete.message": "確定要刪除課程類型嗎?",
|
||||
"Lesson Type": "課程類型",
|
||||
"dashboard.lessonTypes.create": "建立課程類型",
|
||||
"dashboard.lessonTypes.create.message": "請輸入課程類型名稱",
|
||||
"dashboard.lessonTypes.create.success": "課程類型建立成功",
|
||||
"dashboard.lessonTypes.create.error": "課程類型建立失敗",
|
||||
"dashboard.lessonTypes.edit.success": "課程類型更新成功",
|
||||
"dashboard.lessonTypes.edit.error": "課程類型更新失敗",
|
||||
"dashboard.lessonTypes.delete.success": "課程類型刪除成功",
|
||||
"dashboard.lessonTypes.delete.error": "課程類型刪除失敗",
|
||||
"dashboard.lessonTypes.list": "課程類型列表",
|
||||
"dashboard.lessonTypes.list.title": "課程類型列表",
|
||||
"dashboard.lessonTypes.list.message": "請選擇課程類型",
|
||||
"dashboard.lessonTypes.list.empty": "目前沒有課程類型",
|
||||
"dashboard.lessonTypes.list.empty.title": "目前沒有課程類型",
|
||||
"dashboard.lessonTypes.list.empty.message": "請建立課程類型",
|
||||
"dashboard.lessonTypes.list.empty.create": "建立課程類型",
|
||||
"Lesson Name": "課程名稱",
|
||||
"dashboard.lessonTypes.list.empty.create.title": "建立課程類型",
|
||||
"dashboard.lessonTypes.list.empty.create.message": "請輸入課程名稱",
|
||||
"dashboard.lessonTypes.list.empty.create.success": "課程建立成功",
|
||||
"dashboard.lessonTypes.list.empty.create.error": "課程建立失敗",
|
||||
"dashboard.lessonTypes.list.empty.create.name": "課程名稱",
|
||||
"dashboard.lessonTypes.list.empty.create.type": "課程類型",
|
||||
"dashboard.lessonTypes.list.empty.create.type.placeholder": "請選擇課程類型",
|
||||
"dashboard.lessonTypes.list.empty.create.type.title": "課程類型",
|
||||
"dashboard.lessonTypes.list.empty.create.type.message": "請選擇課程類型",
|
||||
"dashboard.lessonTypes.list.empty.create.type.empty": "目前沒有課程類型",
|
||||
"dashboard.lessonTypes.list.empty.create.type.empty.title": "目前沒有課程類型",
|
||||
"dashboard.lessonTypes.list.empty.create.type.empty.message": "請建立課程類型",
|
||||
"dashboard.lessonTypes.list.empty.create.type.empty.create": "建立課程類型",
|
||||
"Position": "位置",
|
||||
"Visible": "顯示",
|
||||
"Lesson Types": "課程類型",
|
||||
"Apply": "套用",
|
||||
"Overview": "概觀",
|
||||
"Dashboard": "儀表板",
|
||||
"Tickets": "票券",
|
||||
"Sign ups": "註冊",
|
||||
"Open issues": "開啟問題",
|
||||
"Closed issues": "關閉問題",
|
||||
"increase": "增加",
|
||||
"decrease": "減少",
|
||||
"common.loading": "載入中",
|
||||
"vs last month": "與上個月相比",
|
||||
"Dashboards": "儀表板",
|
||||
"App usage": "應用程式使用情況",
|
||||
"Find your dream job": "找到你的夢想工作",
|
||||
"Need help figuring things out?": "需要幫助嗎?",
|
||||
"Search for jobs that match your skills and apply to them directly": "搜索符合你技能的工作,直接申請",
|
||||
"Find answers to your questions and get in touch with our team.": "找到你的問題並與我們的團隊聯繫",
|
||||
"Explore documentation": "探索文件",
|
||||
"Learn how to get started with our product and make the most of it.": "學習如何開始使用我們的產品並充分利用它",
|
||||
"Search Jobs": "搜索工作",
|
||||
"Help Center": "幫助中心",
|
||||
"Documentation": "文件",
|
||||
"increase_in_app_usage_with": "使用者量增加",
|
||||
"new_products_purchased": "新產品購買",
|
||||
"Our subscriptions": "我們的訂閱",
|
||||
"this_year": "今年",
|
||||
"is_forecasted_to_increase_in_your_traffic_by_the_end_of_the_current_month": "今年預計增加你的流量",
|
||||
"Jan": "一月",
|
||||
"Feb": "二月",
|
||||
"Mar": "三月",
|
||||
"Apr": "四月",
|
||||
"May": "五月",
|
||||
"Jun": "六月",
|
||||
"Jul": "七月",
|
||||
"Aug": "八月",
|
||||
"Sep": "九月",
|
||||
"Oct": "十月",
|
||||
"Nov": "十一月",
|
||||
"Dec": "十二月",
|
||||
"See all subscriptions": "查看所有訂閱",
|
||||
"app_chat": "應用程式聊天",
|
||||
"Upcoming events": "即將發生的事件",
|
||||
"Based on the linked bank accounts": "基於連結的銀行帳戶",
|
||||
"App limits": "應用程式限制",
|
||||
"You have used {percentage} of your available spots.": "您已使用 {percentage} 的可用座位。升級計劃以建立更多專案",
|
||||
"userMessagesUnread": "hello userMessagesUnread",
|
||||
"Upgrade plan to create more projects": "升級計劃以建立更多專案",
|
||||
"You have almost reached your limit": "您已接近您的限制",
|
||||
"Upgrade plan": "升級計劃",
|
||||
"Help center": "幫助中心",
|
||||
"Jobs": "工作",
|
||||
"go_to_chat": "前往聊天",
|
||||
"See all events": "查看所有事件",
|
||||
"You have used": "您已使用",
|
||||
"of your available spots": "的可用座位",
|
||||
"Paid": "已付款",
|
||||
"Canceled": "已取消",
|
||||
"Pending": "待付款",
|
||||
"Expiring": "已過期",
|
||||
"Refunded": "已退款",
|
||||
"Total": "總計",
|
||||
"Total paid": "已付款",
|
||||
"Total cancelled": "已取消",
|
||||
"Total pending": "待付款",
|
||||
"year": "年",
|
||||
"month": "月",
|
||||
"week": "週",
|
||||
"day": "日",
|
||||
"hour": "小時",
|
||||
"minute": "分鐘",
|
||||
"second": "秒",
|
||||
"ago": "前",
|
||||
"remaining": "剩下",
|
||||
"days": "天",
|
||||
"hours": "小時",
|
||||
"minutes": "分鐘",
|
||||
"seconds": "秒",
|
||||
"Clear filters": "清除篩選",
|
||||
"Type information": "輸入資訊",
|
||||
"dashboard.lessonTypes.create.typeInformation": "輸入課程類型資訊",
|
||||
"dashboard.lessonTypes.create.avatar": "上傳課程類型圖片",
|
||||
"dashboard.lessonTypes.create.avatarRequirements": "上傳課程類型圖片",
|
||||
"dashboard.lessonTypes.create.select": "請選擇",
|
||||
"dashboard.lessonTypes.create.name": "課程類型名稱",
|
||||
"dashboard.lessonTypes.create.type": "課程類型",
|
||||
"dashboard.lessonTypes.create.position": "課程類型順序",
|
||||
"dashboard.lessonTypes.create.visibleToUser": "顯示給使用者",
|
||||
"dashboard.lessonTypes.edit.typeInformation": "輸入課程類型資訊",
|
||||
"dashboard.lessonTypes.edit.avatar": "上傳課程類型圖片",
|
||||
"dashboard.lessonTypes.edit.avatarRequirements": "上傳課程類型圖片",
|
||||
"dashboard.lessonTypes.edit.select": "請選擇",
|
||||
"dashboard.lessonTypes.edit.name": "課程類型名稱",
|
||||
"dashboard.lessonTypes.edit.type": "課程類型",
|
||||
"dashboard.lessonTypes.edit.position": "課程類型順序",
|
||||
"dashboard.lessonTypes.edit.visibleToUser": "顯示給使用者",
|
||||
"dashboard.lessonTypes.edit.visible": "顯示",
|
||||
"dashboard.lessonTypes.edit.hidden": "隱藏",
|
||||
"dashboard.lessonTypes.edit.cancelButton": "取消",
|
||||
"dashboard.lessonTypes.edit.updateButton": "更新",
|
||||
"Delete Lesson Type ?": "刪除課程類型?",
|
||||
"Are you sure you want to delete lesson type ?": "確定要刪除課程類型嗎?",
|
||||
"Cancel": "取消",
|
||||
"Delete": "刪除",
|
||||
"All": "全部",
|
||||
"categories": "問題分類",
|
||||
"listening-practice": "聽講練習 (LP)",
|
||||
"matching-frenzy": "配對練習 (MF)",
|
||||
"connective-revision": "連接詞練習 (CR)",
|
||||
"teachers": "導師",
|
||||
"questions": "題目",
|
||||
"loading": "載入中",
|
||||
"Notifications": "通知",
|
||||
"Mark all as read": "標記所有為已讀",
|
||||
"There are no notifications": "沒有通知",
|
||||
"listenings": "聽講 (Listenings)",
|
||||
"question_category": "問題分類",
|
||||
"question_list": "問題",
|
||||
"matching_frenzy": "配對 (Matching Frenzy)",
|
||||
"connective_revision": "連接詞 (Connective Revision)",
|
||||
"settings": "設定",
|
||||
"students": "學生",
|
||||
"dashboard.lessonCategories.add": "新增課程分類",
|
||||
"dashboard.lessonCategories.title": "課程分類",
|
||||
"dashboard.lessonCategorys.edit.name": "課程分類名稱",
|
||||
"dashboard.lessonCategorys.edit.position": "課程分類順序",
|
||||
"dashboard.lessonCategorys.edit.title": "編輯課程分類",
|
||||
"dashboard.lessonCategorys.edit.visibleToUser": "顯示給使用者",
|
||||
"dashboard.lessonCategorys.edit.visible": "顯示",
|
||||
"dashboard.lessonCategorys.edit.hidden": "隱藏",
|
||||
"dashboard.lessonCategorys.edit.cancelButton": "取消",
|
||||
"dashboard.lessonCategorys.edit.updateButton": "更新",
|
||||
"dashboard.lessonCategorys.list.title": "課堂種類",
|
||||
"word-count": "字數",
|
||||
"dashboard.lessonTypes.list.error": "課程類型載入失敗",
|
||||
"unable-to-process-request": "無法處理請求",
|
||||
"detailed-error-information": "詳細錯誤資訊"
|
||||
}
|
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
|
||||
|
||||
set -ex
|
||||
set -x
|
||||
|
||||
# ls **/.next
|
||||
# ls **/.pnpm
|
||||
# ls **/node_modules
|
||||
ls **/*.bak
|
||||
ls **/*draft
|
||||
ls **/*.del
|
||||
ls **/*.plan
|
||||
ls **/*Zone*
|
||||
|
||||
set -e
|
||||
# set -e
|
||||
|
||||
read -p "Press [Enter] key to clean directories..."
|
||||
# read -p "Press [Enter] key to clean directories..."
|
||||
|
||||
# rm -rf **/.next
|
||||
# rm -rf **/.pnpm
|
||||
# rm -rf **/node_modules
|
||||
rm -rf **/*Zone*
|
||||
# # rm -rf **/.next
|
||||
# # rm -rf **/.pnpm
|
||||
# # rm -rf **/node_modules
|
||||
# rm -rf **/*Zone*
|
||||
|
||||
echo "clean done"
|
||||
|
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();
|
||||
});
|
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>
|
48
002_source/cms/src/app/dashboard/connectives/create/page.tsx
Normal file
48
002_source/cms/src/app/dashboard/connectives/create/page.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
'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 { LessonCategoryCreateForm } from '@/components/dashboard/lesson_category/lesson-category-create-form';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
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.lesson_categories.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
{t('title', { ns: 'lesson_category' })}
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h4">{t('create.title', { ns: 'lesson_category' })}</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<LessonCategoryCreateForm />
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
'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 { LessonCategoryEditForm } from '@/components/dashboard/lesson_category/lesson-category-edit-form';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
const { t } = useTranslation(['lesson_category']);
|
||||
|
||||
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.lesson_categories.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
{t('edit.title', { ns: 'lesson_category' })}
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h4">{t('edit.title', { ns: 'lesson_category' })}</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<LessonCategoryEditForm />
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||
|
||||
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
||||
|
||||
// import type { LessonCategory } from '@/components/dashboard/lesson_category/lesson-categories-table';
|
||||
// import type { LessonCategory } from '@/components/dashboard/lesson_category/interfaces';
|
||||
|
||||
export const lessonCategoriesSampleData = [
|
||||
{
|
||||
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[];
|
@@ -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,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>
|
||||
);
|
||||
}
|
276
002_source/cms/src/app/dashboard/cr/categories/list/page.tsx
Normal file
276
002_source/cms/src/app/dashboard/cr/categories/list/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,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[];
|
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
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[];
|
@@ -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 { CrQuestionCreateForm } from '@/components/dashboard/cr/questions/cr-question-create-form';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
// RULES: follow the name of page directory
|
||||
const { t } = useTranslation(['lp_questions']);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
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.lp_questions.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>
|
||||
<CrQuestionCreateForm />
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
# task
|
||||
|
||||
## instruction
|
||||
|
||||
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/_helloworld/page.tsx`
|
||||
|
||||
with reference to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_types/edit/[typeId]/page.tsx`
|
||||
|
||||
please modify `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/app/dashboard/lesson_categories/edit/page.tsx`
|
||||
|
||||
please draft a tsx for showing error to user thanks,
|
@@ -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 { CrQuestionEditForm } from '@/components/dashboard/cr/questions/cr-question-edit-form';
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
const { t } = useTranslation(['lp_questions']);
|
||||
|
||||
React.useEffect(() => {
|
||||
// console.log('helloworld');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
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.lp_questions.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>
|
||||
<CrQuestionEditForm />
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
272
002_source/cms/src/app/dashboard/cr/questions/page.tsx
Normal file
272
002_source/cms/src/app/dashboard/cr/questions/page.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
'use client';
|
||||
|
||||
// RULES:
|
||||
// contains list page for cr_questions (QuizCRQuestions)
|
||||
// contain definition to collection only
|
||||
//
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { COL_QUIZ_CR_QUESTIONS } from '@/constants';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
import type { ListResult, RecordModel } from 'pocketbase';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import isDevelopment from '@/lib/check-is-development';
|
||||
import { logger } from '@/lib/default-logger';
|
||||
import { pb } from '@/lib/pb';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
import ErrorDisplay from '@/components/dashboard/error';
|
||||
import { defaultCrQuestion } from '@/components/dashboard/cr/questions/_constants';
|
||||
import { CrQuestionsFilters } from '@/components/dashboard/cr/questions/cr-questions-filters';
|
||||
import type { Filters } from '@/components/dashboard/cr/questions/cr-questions-filters';
|
||||
import { CrQuestionsPagination } from '@/components/dashboard/cr/questions/cr-questions-pagination';
|
||||
import { CrQuestionsSelectionProvider } from '@/components/dashboard/cr/questions/cr-questions-selection-context';
|
||||
import { CrQuestionsTable } from '@/components/dashboard/cr/questions/cr-questions-table';
|
||||
import type { CrQuestion } from '@/components/dashboard/cr/questions/type';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const { t } = useTranslation(['lp_questions']);
|
||||
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||
const router = useRouter();
|
||||
const [lessonQuestionsData, setLessonCategoriesData] = React.useState<CrQuestion[]>([]);
|
||||
//
|
||||
|
||||
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
||||
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||
//
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||
//
|
||||
const [f, setF] = React.useState<CrQuestion[]>([]);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||
//
|
||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||
const [listOption, setListOption] = React.useState({});
|
||||
const [listSort, setListSort] = React.useState({});
|
||||
|
||||
//
|
||||
const sortedLessonCategories = applySort(lessonQuestionsData, sortDir);
|
||||
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status });
|
||||
|
||||
const reloadRows = async (): Promise<void> => {
|
||||
try {
|
||||
const models: ListResult<RecordModel> = await pb
|
||||
.collection(COL_QUIZ_CR_QUESTIONS)
|
||||
.getList(currentPage + 1, rowsPerPage, listOption);
|
||||
const { items, totalItems } = models;
|
||||
const tempLessonTypes: CrQuestion[] = items.map((lt) => {
|
||||
return { ...defaultCrQuestion, ...lt };
|
||||
});
|
||||
|
||||
setLessonCategoriesData(tempLessonTypes);
|
||||
setRecordCount(totalItems);
|
||||
setF(tempLessonTypes);
|
||||
// console.log({ currentPage, f });
|
||||
} catch (error) {
|
||||
//
|
||||
logger.error(error);
|
||||
setShowError({
|
||||
//
|
||||
show: true,
|
||||
detail: JSON.stringify(error, null, 2),
|
||||
});
|
||||
} finally {
|
||||
setShowLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const [lastListOption, setLastListOption] = React.useState({});
|
||||
const isFirstRun = React.useRef(false);
|
||||
React.useEffect(() => {
|
||||
if (!isFirstRun.current) {
|
||||
isFirstRun.current = true;
|
||||
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
||||
// reset page number as tab changes
|
||||
setLastListOption(listOption);
|
||||
setCurrentPage(0);
|
||||
void reloadRows();
|
||||
} else {
|
||||
void reloadRows();
|
||||
}
|
||||
}, [currentPage, rowsPerPage, listOption]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let tempFilter = [],
|
||||
tempSortDir = '';
|
||||
|
||||
if (visible) {
|
||||
tempFilter.push(`visible = "${visible}"`);
|
||||
}
|
||||
|
||||
if (sortDir) {
|
||||
tempSortDir = `-created`;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
tempFilter.push(`name ~ "%${name}%"`);
|
||||
}
|
||||
|
||||
if (type) {
|
||||
tempFilter.push(`type ~ "%${type}%"`);
|
||||
}
|
||||
|
||||
let preFinalListOption = {};
|
||||
if (tempFilter.length > 0) {
|
||||
preFinalListOption = { filter: tempFilter.join(' && ') };
|
||||
}
|
||||
if (tempSortDir.length > 0) {
|
||||
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
|
||||
}
|
||||
setListOption(preFinalListOption);
|
||||
// setListOption({
|
||||
// filter: tempFilter.join(' && '),
|
||||
// sort: tempSortDir,
|
||||
// //
|
||||
// });
|
||||
}, [visible, sortDir, name, type]);
|
||||
|
||||
// return <>helloworld</>;
|
||||
|
||||
if (showLoading) return <FormLoading />;
|
||||
|
||||
if (showError.show)
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request')}
|
||||
code={-1}
|
||||
details={showError.detail}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={3}
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
>
|
||||
<Box sx={{ flex: '1 1 auto' }}>
|
||||
<Typography variant="h4">{t('list.title')}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<LoadingButton
|
||||
loading={isLoadingAddPage}
|
||||
onClick={(): void => {
|
||||
setIsLoadingAddPage(true);
|
||||
router.push(paths.dashboard.cr_questions.create);
|
||||
}}
|
||||
startIcon={<PlusIcon />}
|
||||
variant="contained"
|
||||
>
|
||||
{t('list.add')}
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<CrQuestionsSelectionProvider lessonQuestions={f}>
|
||||
<Card>
|
||||
<CrQuestionsFilters
|
||||
filters={{ email, phone, status, name, visible, type }}
|
||||
fullData={lessonQuestionsData}
|
||||
sortDir={sortDir}
|
||||
/>
|
||||
<Divider />
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<CrQuestionsTable
|
||||
reloadRows={reloadRows}
|
||||
rows={f}
|
||||
/>
|
||||
</Box>
|
||||
<Divider />
|
||||
<CrQuestionsPagination
|
||||
count={recordCount}
|
||||
page={currentPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
setPage={setCurrentPage}
|
||||
setRowsPerPage={setRowsPerPage}
|
||||
/>
|
||||
</Card>
|
||||
</CrQuestionsSelectionProvider>
|
||||
</Stack>
|
||||
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
|
||||
<pre>{JSON.stringify(f, null, 2)}</pre>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Sorting and filtering has to be done on the server.
|
||||
|
||||
function applySort(row: CrQuestion[], sortDir: 'asc' | 'desc' | undefined): CrQuestion[] {
|
||||
return row.sort((a, b) => {
|
||||
if (sortDir === 'asc') {
|
||||
return a.createdAt.getTime() - b.createdAt.getTime();
|
||||
}
|
||||
|
||||
return b.createdAt.getTime() - a.createdAt.getTime();
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters(row: CrQuestion[], { email, phone, status, name, visible }: Filters): CrQuestion[] {
|
||||
return row.filter((item) => {
|
||||
if (email) {
|
||||
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (phone) {
|
||||
if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (status) {
|
||||
if (item.status !== status) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (name) {
|
||||
if (!item.name?.toLowerCase().includes(name.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
searchParams: {
|
||||
email?: string;
|
||||
phone?: string;
|
||||
sortDir?: 'asc' | 'desc';
|
||||
status?: string;
|
||||
name?: string;
|
||||
visible?: string;
|
||||
type?: string;
|
||||
//
|
||||
};
|
||||
}
|
@@ -1,308 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import RouterLink from 'next/link';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||
import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard';
|
||||
import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House';
|
||||
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
||||
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { paths } from '@/paths';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { PropertyItem } from '@/components/core/property-item';
|
||||
import { PropertyList } from '@/components/core/property-list';
|
||||
import { Notifications } from '@/components/dashboard/customer/notifications';
|
||||
import { Payments } from '@/components/dashboard/customer/payments';
|
||||
import type { Address } from '@/components/dashboard/customer/shipping-address';
|
||||
import { ShippingAddress } from '@/components/dashboard/customer/shipping-address';
|
||||
|
||||
export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
|
||||
export default function Page(): React.JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack spacing={3}>
|
||||
<div>
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.customers.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
Customers
|
||||
</Link>
|
||||
</div>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
|
||||
<Avatar src="/assets/avatar-1.png" sx={{ '--Avatar-size': '64px' }}>
|
||||
MV
|
||||
</Avatar>
|
||||
<div>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Typography variant="h4">Miron Vitold</Typography>
|
||||
<Chip
|
||||
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
|
||||
label="Active"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Stack>
|
||||
<Typography color="text.secondary" variant="body1">
|
||||
miron.vitold@domain.com
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<div>
|
||||
<Button endIcon={<CaretDownIcon />} variant="contained">
|
||||
Action
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Grid container spacing={4}>
|
||||
<Grid lg={4} xs={12}>
|
||||
<Stack spacing={4}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton>
|
||||
<PencilSimpleIcon />
|
||||
</IconButton>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Basic details"
|
||||
/>
|
||||
<PropertyList
|
||||
divider={<Divider />}
|
||||
orientation="vertical"
|
||||
sx={{ '--PropertyItem-padding': '12px 24px' }}
|
||||
>
|
||||
{(
|
||||
[
|
||||
{ key: 'Customer ID', value: <Chip label="USR-001" size="small" variant="soft" /> },
|
||||
{ key: 'Name', value: 'Miron Vitold' },
|
||||
{ key: 'Email', value: 'miron.vitold@domain.com' },
|
||||
{ key: 'Phone', value: '(425) 434-5535' },
|
||||
{ key: 'Company', value: 'Devias IO' },
|
||||
{
|
||||
key: 'Quota',
|
||||
value: (
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" />
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
50%
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Security"
|
||||
/>
|
||||
<CardContent>
|
||||
<Stack spacing={1}>
|
||||
<div>
|
||||
<Button color="error" variant="contained">
|
||||
Delete account
|
||||
</Button>
|
||||
</div>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
A deleted customer cannot be restored. All data will be permanently removed.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid lg={8} xs={12}>
|
||||
<Stack spacing={4}>
|
||||
<Payments
|
||||
ordersValue={2069.48}
|
||||
payments={[
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 500,
|
||||
invoiceId: 'INV-005',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(5, 'minute').subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 324.5,
|
||||
invoiceId: 'INV-004',
|
||||
status: 'refunded',
|
||||
createdAt: dayjs().subtract(21, 'minute').subtract(2, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 746.5,
|
||||
invoiceId: 'INV-003',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(7, 'minute').subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 56.89,
|
||||
invoiceId: 'INV-002',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(48, 'minute').subtract(4, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
currency: 'USD',
|
||||
amount: 541.59,
|
||||
invoiceId: 'INV-001',
|
||||
status: 'completed',
|
||||
createdAt: dayjs().subtract(31, 'minute').subtract(5, 'hour').toDate(),
|
||||
},
|
||||
]}
|
||||
refundsValue={324.5}
|
||||
totalOrders={5}
|
||||
/>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
|
||||
Edit
|
||||
</Button>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Billing details"
|
||||
/>
|
||||
<CardContent>
|
||||
<Card sx={{ borderRadius: 1 }} variant="outlined">
|
||||
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}>
|
||||
{(
|
||||
[
|
||||
{ key: 'Credit card', value: '**** 4142' },
|
||||
{ key: 'Country', value: 'United States' },
|
||||
{ key: 'State', value: 'Michigan' },
|
||||
{ key: 'City', value: 'Southfield' },
|
||||
{ key: 'Address', value: '1721 Bartlett Avenue, 48034' },
|
||||
{ key: 'Tax ID', value: 'EU87956621' },
|
||||
] satisfies { key: string; value: React.ReactNode }[]
|
||||
).map(
|
||||
(item): React.JSX.Element => (
|
||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
||||
)
|
||||
)}
|
||||
</PropertyList>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<Button color="secondary" startIcon={<PlusIcon />}>
|
||||
Add
|
||||
</Button>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<HouseIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Shipping addresses"
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid container spacing={3}>
|
||||
{(
|
||||
[
|
||||
{
|
||||
id: 'ADR-001',
|
||||
country: 'United States',
|
||||
state: 'Michigan',
|
||||
city: 'Lansing',
|
||||
zipCode: '48933',
|
||||
street: '480 Haven Lane',
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
id: 'ADR-002',
|
||||
country: 'United States',
|
||||
state: 'Missouri',
|
||||
city: 'Springfield',
|
||||
zipCode: '65804',
|
||||
street: '4807 Lighthouse Drive',
|
||||
},
|
||||
] satisfies Address[]
|
||||
).map((address) => (
|
||||
<Grid key={address.id} md={6} xs={12}>
|
||||
<ShippingAddress address={address} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Notifications
|
||||
notifications={[
|
||||
{
|
||||
id: 'EV-002',
|
||||
type: 'Refund request approved',
|
||||
status: 'pending',
|
||||
createdAt: dayjs().subtract(34, 'minute').subtract(5, 'hour').subtract(3, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'EV-001',
|
||||
type: 'Order confirmation',
|
||||
status: 'delivered',
|
||||
createdAt: dayjs().subtract(49, 'minute').subtract(11, 'hour').subtract(4, 'day').toDate(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -1,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';
|
||||
import type { Metadata } from 'next';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
|
||||
import { config } from '@/config';
|
||||
// src/app/dashboard/customers/page.tsx
|
||||
'use client';
|
||||
import type { Customer } from '@/components/dashboard/customer/type.d';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { CustomersFilters } from '@/components/dashboard/customer/customers-filters';
|
||||
import type { Filters } from '@/components/dashboard/customer/customers-filters';
|
||||
import { CustomersPagination } from '@/components/dashboard/customer/customers-pagination';
|
||||
import { CustomersSelectionProvider } from '@/components/dashboard/customer/customers-selection-context';
|
||||
import { CustomersTable } from '@/components/dashboard/customer/customers-table';
|
||||
import type { Customer } from '@/components/dashboard/customer/customers-table';
|
||||
|
||||
export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
|
||||
const customers = [
|
||||
export const SampleCustomers = [
|
||||
{
|
||||
id: 'USR-005',
|
||||
name: 'Fran Perez',
|
||||
@@ -171,85 +155,3 @@ const customers = [
|
||||
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
||||
},
|
||||
] satisfies Customer[];
|
||||
|
||||
interface PageProps {
|
||||
searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string };
|
||||
}
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const { email, phone, sortDir, status } = searchParams;
|
||||
|
||||
const sortedCustomers = applySort(customers, sortDir);
|
||||
const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status });
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'var(--Content-maxWidth)',
|
||||
m: 'var(--Content-margin)',
|
||||
p: 'var(--Content-padding)',
|
||||
width: 'var(--Content-width)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||
<Box sx={{ flex: '1 1 auto' }}>
|
||||
<Typography variant="h4">Customers</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button startIcon={<PlusIcon />} variant="contained">
|
||||
Add
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
<CustomersSelectionProvider customers={filteredCustomers}>
|
||||
<Card>
|
||||
<CustomersFilters filters={{ email, phone, status }} sortDir={sortDir} />
|
||||
<Divider />
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<CustomersTable rows={filteredCustomers} />
|
||||
</Box>
|
||||
<Divider />
|
||||
<CustomersPagination count={filteredCustomers.length + 100} page={0} />
|
||||
</Card>
|
||||
</CustomersSelectionProvider>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Sorting and filtering has to be done on the server.
|
||||
|
||||
function applySort(row: Customer[], sortDir: 'asc' | 'desc' | undefined): Customer[] {
|
||||
return row.sort((a, b) => {
|
||||
if (sortDir === 'asc') {
|
||||
return a.createdAt.getTime() - b.createdAt.getTime();
|
||||
}
|
||||
|
||||
return b.createdAt.getTime() - a.createdAt.getTime();
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters(row: Customer[], { email, phone, status }: Filters): Customer[] {
|
||||
return row.filter((item) => {
|
||||
if (email) {
|
||||
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (phone) {
|
||||
if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (status) {
|
||||
if (item.status !== status) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
@@ -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 type { Metadata } from 'next';
|
||||
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 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 type { RecordModel } from 'pocketbase';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
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';
|
||||
import { logger } from '@/lib/default-logger';
|
||||
import { pb } from '@/lib/pb';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
|
||||
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 {
|
||||
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 (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -61,244 +101,38 @@ export default function Page(): React.JSX.Element {
|
||||
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' }}
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={3}
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
>
|
||||
{(
|
||||
[
|
||||
{ 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>
|
||||
<TitleCard lpModel={showLessonCategory} />
|
||||
</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}>
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
>
|
||||
<Grid
|
||||
lg={4}
|
||||
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}
|
||||
<BasicDetailCard
|
||||
lpModel={showLessonCategory}
|
||||
handleEditClick={handleEditClick}
|
||||
/>
|
||||
<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} />
|
||||
<SampleSecurityCard />
|
||||
</Stack>
|
||||
</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(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Grid
|
||||
lg={8}
|
||||
xs={12}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<SamplePaymentCard />
|
||||
<SampleAddressCard />
|
||||
<Notifications notifications={SampleNotifications} />
|
||||
</Stack>
|
||||
</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';
|
||||
|
||||
// RULES:
|
||||
// contains list page for customers (Customers)
|
||||
// contain definition to collection only
|
||||
//
|
||||
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 Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
@@ -9,184 +16,140 @@ 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 { config } from '@/config';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { CustomersFilters } from '@/components/dashboard/customer/customers-filters';
|
||||
import type { Filters } from '@/components/dashboard/customer/customers-filters';
|
||||
// import 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';
|
||||
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;
|
||||
|
||||
const customers = [
|
||||
{
|
||||
id: 'USR-005',
|
||||
name: 'Fran Perez',
|
||||
avatar: '/assets/avatar-5.png',
|
||||
email: 'fran.perez@domain.com',
|
||||
phone: '(815) 704-0045',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-004',
|
||||
name: 'Penjani Inyene',
|
||||
avatar: '/assets/avatar-4.png',
|
||||
email: 'penjani.inyene@domain.com',
|
||||
phone: '(803) 937-8925',
|
||||
quota: 100,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-003',
|
||||
name: 'Carson Darrin',
|
||||
avatar: '/assets/avatar-3.png',
|
||||
email: 'carson.darrin@domain.com',
|
||||
phone: '(715) 278-5041',
|
||||
quota: 10,
|
||||
status: 'blocked',
|
||||
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-002',
|
||||
name: 'Siegbert Gottfried',
|
||||
avatar: '/assets/avatar-2.png',
|
||||
email: 'siegbert.gottfried@domain.com',
|
||||
phone: '(603) 766-0431',
|
||||
quota: 0,
|
||||
status: 'pending',
|
||||
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-001',
|
||||
name: 'Miron Vitold',
|
||||
avatar: '/assets/avatar-1.png',
|
||||
email: 'miron.vitold@domain.com',
|
||||
phone: '(425) 434-5535',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-005',
|
||||
name: 'Fran Perez',
|
||||
avatar: '/assets/avatar-5.png',
|
||||
email: 'fran.perez@domain.com',
|
||||
phone: '(815) 704-0045',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-004',
|
||||
name: 'Penjani Inyene',
|
||||
avatar: '/assets/avatar-4.png',
|
||||
email: 'penjani.inyene@domain.com',
|
||||
phone: '(803) 937-8925',
|
||||
quota: 100,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-003',
|
||||
name: 'Carson Darrin',
|
||||
avatar: '/assets/avatar-3.png',
|
||||
email: 'carson.darrin@domain.com',
|
||||
phone: '(715) 278-5041',
|
||||
quota: 10,
|
||||
status: 'blocked',
|
||||
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-002',
|
||||
name: 'Siegbert Gottfried',
|
||||
avatar: '/assets/avatar-2.png',
|
||||
email: 'siegbert.gottfried@domain.com',
|
||||
phone: '(603) 766-0431',
|
||||
quota: 0,
|
||||
status: 'pending',
|
||||
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-001',
|
||||
name: 'Miron Vitold',
|
||||
avatar: '/assets/avatar-1.png',
|
||||
email: 'miron.vitold@domain.com',
|
||||
phone: '(425) 434-5535',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-005',
|
||||
name: 'Fran Perez',
|
||||
avatar: '/assets/avatar-5.png',
|
||||
email: 'fran.perez@domain.com',
|
||||
phone: '(815) 704-0045',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(1, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-004',
|
||||
name: 'Penjani Inyene',
|
||||
avatar: '/assets/avatar-4.png',
|
||||
email: 'penjani.inyene@domain.com',
|
||||
phone: '(803) 937-8925',
|
||||
quota: 100,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(3, 'hour').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-003',
|
||||
name: 'Carson Darrin',
|
||||
avatar: '/assets/avatar-3.png',
|
||||
email: 'carson.darrin@domain.com',
|
||||
phone: '(715) 278-5041',
|
||||
quota: 10,
|
||||
status: 'blocked',
|
||||
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-002',
|
||||
name: 'Siegbert Gottfried',
|
||||
avatar: '/assets/avatar-2.png',
|
||||
email: 'siegbert.gottfried@domain.com',
|
||||
phone: '(603) 766-0431',
|
||||
quota: 0,
|
||||
status: 'pending',
|
||||
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
|
||||
},
|
||||
{
|
||||
id: 'USR-001',
|
||||
name: 'Miron Vitold',
|
||||
avatar: '/assets/avatar-1.png',
|
||||
email: 'miron.vitold@domain.com',
|
||||
phone: '(425) 434-5535',
|
||||
quota: 50,
|
||||
status: 'active',
|
||||
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
|
||||
},
|
||||
] satisfies Customer[];
|
||||
|
||||
interface PageProps {
|
||||
searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string };
|
||||
}
|
||||
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 { defaultCustomer } from '@/components/dashboard/customer/_constants';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const { t } = useTranslation(['customers']);
|
||||
const router = useRouter();
|
||||
|
||||
const { email, phone, sortDir, status } = searchParams;
|
||||
|
||||
const sortedCustomers = applySort(customers, sortDir);
|
||||
const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status });
|
||||
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<Customer[]>([]);
|
||||
//
|
||||
|
||||
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(() => {
|
||||
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 (
|
||||
<Box
|
||||
@@ -204,35 +167,50 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
>
|
||||
<Box sx={{ flex: '1 1 auto' }}>
|
||||
<Typography variant="h4">Customers</Typography>
|
||||
<Typography variant="h4">{t('list.title')}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
<LoadingButton
|
||||
loading={isLoadingAddPage}
|
||||
onClick={(): void => {
|
||||
setIsLoadingAddPage(true);
|
||||
router.push(paths.dashboard.customers.create);
|
||||
}}
|
||||
startIcon={<PlusIcon />}
|
||||
variant="contained"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
{t('list.add')}
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<CustomersSelectionProvider customers={filteredCustomers}>
|
||||
<CustomersSelectionProvider customers={f}>
|
||||
<Card>
|
||||
<CustomersFilters
|
||||
filters={{ email, phone, status }}
|
||||
fullData={lessonCategoriesData}
|
||||
sortDir={sortDir}
|
||||
/>
|
||||
<Divider />
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<CustomersTable rows={filteredCustomers} />
|
||||
<CustomersTable
|
||||
reloadRows={reloadRows}
|
||||
rows={f}
|
||||
/>
|
||||
</Box>
|
||||
<Divider />
|
||||
<CustomersPagination
|
||||
count={filteredCustomers.length + 100}
|
||||
page={0}
|
||||
count={recordCount}
|
||||
page={currentPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
setPage={setCurrentPage}
|
||||
setRowsPerPage={setRowsPerPage}
|
||||
/>
|
||||
</Card>
|
||||
</CustomersSelectionProvider>
|
||||
</Stack>
|
||||
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
|
||||
<pre>{JSON.stringify(f, null, 2)}</pre>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -272,3 +250,7 @@ function applyFilters(row: Customer[], { email, phone, status }: Filters): Custo
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string };
|
||||
}
|
||||
|
302
002_source/cms/src/app/dashboard/lesson_categories/list/page.tsx
Normal file
302
002_source/cms/src/app/dashboard/lesson_categories/list/page.tsx
Normal file
@@ -0,0 +1,302 @@
|
||||
'use client';
|
||||
|
||||
// RULES:
|
||||
// contains list page for lesson_categories (LessonCategories)
|
||||
// contain definition to collection only
|
||||
//
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { COL_LESSON_CATEGORIES } from '@/constants';
|
||||
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 { defaultLessonCategory } from '@/components/dashboard/lesson_category/_constants';
|
||||
import { LessonCategoriesFilters } from '@/components/dashboard/lesson_category/lesson-categories-filters';
|
||||
import type { Filters } from '@/components/dashboard/lesson_category/lesson-categories-filters';
|
||||
import { LessonCategoriesPagination } from '@/components/dashboard/lesson_category/lesson-categories-pagination';
|
||||
import { LessonCategoriesSelectionProvider } from '@/components/dashboard/lesson_category/lesson-categories-selection-context';
|
||||
import { LessonCategoriesTable } from '@/components/dashboard/lesson_category/lesson-categories-table';
|
||||
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
// import { lessonCategoriesSampleData } from './lesson-categories-sample-data';
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const { t } = useTranslation(['lesson_category']);
|
||||
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||
const router = useRouter();
|
||||
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<LessonCategory[]>([]);
|
||||
//
|
||||
|
||||
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<LessonCategory[]>([]);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(1);
|
||||
//
|
||||
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_LESSON_CATEGORIES)
|
||||
.getList(currentPage + 1, rowsPerPage, listOption);
|
||||
const { items, totalItems } = models;
|
||||
const tempLessonTypes: LessonCategory[] = items.map((lt) => {
|
||||
return { ...defaultLessonCategory, ...lt };
|
||||
});
|
||||
|
||||
setLessonCategoriesData(tempLessonTypes);
|
||||
setRecordCount(totalItems);
|
||||
setF(tempLessonTypes);
|
||||
// console.log({ currentPage, f });
|
||||
} catch (error) {
|
||||
//
|
||||
logger.error(error);
|
||||
setShowError({
|
||||
//
|
||||
show: true,
|
||||
detail: JSON.stringify(error, null, 2),
|
||||
});
|
||||
} finally {
|
||||
setShowLoading(false);
|
||||
}
|
||||
|
||||
// pb.collection(COL_LESSON_CATEGORIES)
|
||||
// .getList(currentPage, rowsPerPage, listOption)
|
||||
// .then((lessonCategories: ListResult<RecordModel>) => {
|
||||
// // console.log(lessonTypes);
|
||||
// const { items, page, perPage, totalItems, totalPages } = lessonCategories;
|
||||
// const tempLessonCategories: LessonCategory[] = items.map((item) => {
|
||||
// return { ...defaultLessonCategory, ...item };
|
||||
// });
|
||||
|
||||
// setLessonCategoriesData(tempLessonCategories);
|
||||
// setRecordCount(totalItems);
|
||||
// setF(tempLessonCategories);
|
||||
// // console.log({ currentPage, f });
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// logger.error(error);
|
||||
// setShowError({
|
||||
// //
|
||||
// show: true,
|
||||
// detail: JSON.stringify(error, null, 2),
|
||||
// });
|
||||
// })
|
||||
// .finally(() => {
|
||||
// setShowLoading(false);
|
||||
// });
|
||||
};
|
||||
|
||||
const [lastListOption, setLastListOption] = React.useState({});
|
||||
const isFirstRun = React.useRef(false);
|
||||
React.useEffect(() => {
|
||||
if (!isFirstRun.current) {
|
||||
isFirstRun.current = true;
|
||||
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
||||
// reset page number as tab changes
|
||||
setLastListOption(listOption);
|
||||
setCurrentPage(0);
|
||||
void reloadRows();
|
||||
} else {
|
||||
void reloadRows();
|
||||
}
|
||||
}, [currentPage, rowsPerPage, listOption]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let tempFilter = [],
|
||||
tempSortDir = '';
|
||||
|
||||
if (visible) {
|
||||
tempFilter.push(`visible = "${visible}"`);
|
||||
}
|
||||
|
||||
if (sortDir) {
|
||||
tempSortDir = `-created`;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
tempFilter.push(`name ~ "%${name}%"`);
|
||||
}
|
||||
|
||||
if (type) {
|
||||
tempFilter.push(`type ~ "%${type}%"`);
|
||||
}
|
||||
|
||||
let preFinalListOption = {};
|
||||
if (tempFilter.length > 0) {
|
||||
preFinalListOption = { filter: tempFilter.join(' && ') };
|
||||
}
|
||||
if (tempSortDir.length > 0) {
|
||||
preFinalListOption = { ...preFinalListOption, sort: tempSortDir };
|
||||
}
|
||||
setListOption(preFinalListOption);
|
||||
// setListOption({
|
||||
// filter: tempFilter.join(' && '),
|
||||
// sort: tempSortDir,
|
||||
// //
|
||||
// });
|
||||
}, [visible, sortDir, name, type]);
|
||||
|
||||
// return <>helloworld</>;
|
||||
|
||||
if (showLoading) return <FormLoading />;
|
||||
|
||||
if (showError.show)
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request')}
|
||||
code="500"
|
||||
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.lesson_categories.create);
|
||||
}}
|
||||
startIcon={<PlusIcon />}
|
||||
variant="contained"
|
||||
>
|
||||
{/* add new lesson type */}
|
||||
{t('list.add')}
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<LessonCategoriesSelectionProvider lessonCategories={f}>
|
||||
<Card>
|
||||
<LessonCategoriesFilters
|
||||
filters={{ email, phone, status, name, visible, type }}
|
||||
fullData={lessonCategoriesData}
|
||||
sortDir={sortDir}
|
||||
/>
|
||||
<Divider />
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<LessonCategoriesTable
|
||||
reloadRows={reloadRows}
|
||||
rows={f}
|
||||
/>
|
||||
</Box>
|
||||
<Divider />
|
||||
<LessonCategoriesPagination
|
||||
count={recordCount}
|
||||
page={currentPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
setPage={setCurrentPage}
|
||||
setRowsPerPage={setRowsPerPage}
|
||||
/>
|
||||
</Card>
|
||||
</LessonCategoriesSelectionProvider>
|
||||
</Stack>
|
||||
<Box sx={{ display: isDevelopment ? 'block' : 'none' }}>
|
||||
<pre>{JSON.stringify(f, null, 2)}</pre>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Sorting and filtering has to be done on the server.
|
||||
|
||||
function applySort(row: LessonCategory[], sortDir: 'asc' | 'desc' | undefined): LessonCategory[] {
|
||||
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: LessonCategory[], { email, phone, status, name, visible }: Filters): LessonCategory[] {
|
||||
return row.filter((item) => {
|
||||
if (email) {
|
||||
if (!item.email?.toLowerCase().includes(email.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (phone) {
|
||||
if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (status) {
|
||||
if (item.status !== status) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (name) {
|
||||
if (!item.name?.toLowerCase().includes(name.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
if (!item.visible?.toLowerCase().includes(visible.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
searchParams: {
|
||||
email?: string;
|
||||
phone?: string;
|
||||
sortDir?: 'asc' | 'desc';
|
||||
status?: string;
|
||||
name?: string;
|
||||
visible?: string;
|
||||
type?: string;
|
||||
};
|
||||
}
|
@@ -1,6 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
// import type { Metadata } from 'next';
|
||||
import RouterLink from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { COL_LESSON_CATEGORIES } from '@/constants';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
@@ -24,20 +28,74 @@ import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr
|
||||
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 { RecordModel } from 'pocketbase';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { paths } from '@/paths';
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { logger } from '@/lib/default-logger';
|
||||
import { pb } from '@/lib/pb';
|
||||
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';
|
||||
import { toast } from '@/components/core/toaster';
|
||||
import ErrorDisplay from '@/components/dashboard/error';
|
||||
import { defaultLessonCategory } from '@/components/dashboard/lesson_category/_constants.ts';
|
||||
// import { defaultLessonCategory } from '@/components/dashboard/lesson_category/defaultLessonCategory';
|
||||
import { Notifications } from '@/components/dashboard/lesson_category/notifications';
|
||||
import { Payments } from '@/components/dashboard/lesson_category/payments';
|
||||
import type { Address } from '@/components/dashboard/lesson_category/shipping-address';
|
||||
import { ShippingAddress } from '@/components/dashboard/lesson_category/shipping-address';
|
||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
||||
import FormLoading from '@/components/loading';
|
||||
|
||||
export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
// export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata;
|
||||
|
||||
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<boolean>(false);
|
||||
//
|
||||
const [showLessonCategory, setShowLessonCategory] = React.useState<LessonCategory>(defaultLessonCategory);
|
||||
|
||||
function handleEditClick() {
|
||||
router.push(paths.dashboard.lesson_categories.edit(showLessonCategory.id));
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (catId) {
|
||||
pb.collection(COL_LESSON_CATEGORIES)
|
||||
.getOne(catId)
|
||||
.then((model: RecordModel) => {
|
||||
setShowLessonCategory({ ...defaultLessonCategory, ...model });
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(err);
|
||||
toast(t('dashboard.lessonTypes.list.error'));
|
||||
|
||||
setShowError(true);
|
||||
})
|
||||
.finally(() => {
|
||||
setShowLoading(false);
|
||||
});
|
||||
}
|
||||
}, [catId]);
|
||||
|
||||
if (showLoading) return <FormLoading />;
|
||||
|
||||
if (showError)
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request', { ns: 'common' })}
|
||||
code="500"
|
||||
details={t('error.detailed-error-information', { ns: 'common' })}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -53,31 +111,35 @@ export default function Page(): React.JSX.Element {
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.customers.list}
|
||||
href={paths.dashboard.lesson_categories.list}
|
||||
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
|
||||
Customers
|
||||
{t('dashboard.lessonCategorys.list.title')}
|
||||
</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
|
||||
src={`http://127.0.0.1:8090/api/files/${showLessonCategory.collectionId}/${showLessonCategory.id}/${showLessonCategory.cat_image}`}
|
||||
sx={{ '--Avatar-size': '64px' }}
|
||||
variant="rounded"
|
||||
>
|
||||
empty
|
||||
</Avatar>
|
||||
<div>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Typography variant="h4">Miron Vitold</Typography>
|
||||
<Typography variant="h4">{showLessonCategory.name}</Typography>
|
||||
<Chip
|
||||
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
|
||||
label="Active"
|
||||
label={showLessonCategory.visible}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Stack>
|
||||
<Typography color="text.secondary" variant="body1">
|
||||
miron.vitold@domain.com
|
||||
{showLessonCategory.id}
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
@@ -94,7 +156,11 @@ export default function Page(): React.JSX.Element {
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleEditClick();
|
||||
}}
|
||||
>
|
||||
<PencilSimpleIcon />
|
||||
</IconButton>
|
||||
}
|
||||
@@ -103,7 +169,7 @@ export default function Page(): React.JSX.Element {
|
||||
<UserIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Basic details"
|
||||
title={t('basic-details', { ns: 'lesson_category' })}
|
||||
/>
|
||||
<PropertyList
|
||||
divider={<Divider />}
|
||||
@@ -112,11 +178,20 @@ export default function Page(): React.JSX.Element {
|
||||
>
|
||||
{(
|
||||
[
|
||||
{ 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: 'Customer ID', value: <Chip label={showLessonCategory.id} size="small" variant="soft" /> },
|
||||
{ key: 'Name', value: showLessonCategory.name },
|
||||
{ key: 'Pos', value: showLessonCategory.pos },
|
||||
{
|
||||
key: 'Visible',
|
||||
value: (
|
||||
<Chip
|
||||
//
|
||||
label={showLessonCategory.visible}
|
||||
size="small"
|
||||
variant="soft"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'Quota',
|
||||
value: (
|
||||
@@ -143,7 +218,7 @@ export default function Page(): React.JSX.Element {
|
||||
<ShieldWarningIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Security"
|
||||
title={t('security', { ns: 'lesson_category' })}
|
||||
/>
|
||||
<CardContent>
|
||||
<Stack spacing={1}>
|
||||
@@ -153,7 +228,7 @@ export default function Page(): React.JSX.Element {
|
||||
</Button>
|
||||
</div>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
A deleted customer cannot be restored. All data will be permanently removed.
|
||||
A deleted lesson category cannot be restored. All data will be permanently removed.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
@@ -216,7 +291,7 @@ export default function Page(): React.JSX.Element {
|
||||
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Billing details"
|
||||
title={t('billing-details', { ns: 'lesson_category' })}
|
||||
/>
|
||||
<CardContent>
|
||||
<Card sx={{ borderRadius: 1 }} variant="outlined">
|
||||
@@ -251,7 +326,7 @@ export default function Page(): React.JSX.Element {
|
||||
<HouseIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Shipping addresses"
|
||||
title={t('shipping-addresses', { ns: 'lesson_category' })}
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid container spacing={3}>
|
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
@@ -34,11 +34,10 @@ import FormLoading from '@/components/loading';
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const { t } = useTranslation(['lp_categories']);
|
||||
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||
const router = useRouter();
|
||||
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<LpCategory[]>([]);
|
||||
//
|
||||
|
||||
const [isLoadingAddPage, setIsLoadingAddPage] = React.useState<boolean>(false);
|
||||
const [showLoading, setShowLoading] = React.useState<boolean>(true);
|
||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -749,18 +749,19 @@ import FormLoading from '@/components/loading';
|
||||
|
||||
export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const { t } = useTranslation(['lp_categories']);
|
||||
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||
const router = useRouter();
|
||||
const { email, phone, sortDir, status, name, visible, type } = searchParams;
|
||||
const [lessonCategoriesData, setLessonCategoriesData] = React.useState<LpCategory[]>([]);
|
||||
//
|
||||
|
||||
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<LpCategory[]>([]);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(1);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||
//
|
||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||
const [listOption, setListOption] = React.useState({});
|
||||
const [listSort, setListSort] = React.useState({});
|
||||
@@ -769,6 +770,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const sortedLessonCategories = applySort(lessonCategoriesData, sortDir);
|
||||
const filteredLessonCategories = applyFilters(sortedLessonCategories, { email, phone, status });
|
||||
|
||||
//
|
||||
const reloadRows = async (): Promise<void> => {
|
||||
try {
|
||||
const models: ListResult<RecordModel> = await pb
|
||||
@@ -856,7 +858,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
return (
|
||||
<ErrorDisplay
|
||||
message={t('error.unable-to-process-request')}
|
||||
code="500"
|
||||
code={-1}
|
||||
details={showError.detail}
|
||||
/>
|
||||
);
|
||||
@@ -1552,8 +1554,10 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
||||
const [showError, setShowError] = React.useState({ show: false, detail: '' });
|
||||
//
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState<number>(5);
|
||||
//
|
||||
const [f, setF] = React.useState<LpQuestion[]>([]);
|
||||
const [currentPage, setCurrentPage] = React.useState<number>(0);
|
||||
//
|
||||
const [recordCount, setRecordCount] = React.useState<number>(0);
|
||||
const [listOption, setListOption] = React.useState({});
|
||||
const [listSort, setListSort] = React.useState({});
|
||||
|
@@ -135,7 +135,11 @@ export default function Layout({ children, params }: LayoutProps): React.JSX.Ele
|
||||
const filteredThreads = filterThreads(threads, labelId);
|
||||
|
||||
return (
|
||||
<MailProvider currentLabelId={labelId} labels={labels} threads={filteredThreads}>
|
||||
<MailProvider
|
||||
currentLabelId={labelId}
|
||||
labels={labels}
|
||||
threads={filteredThreads}
|
||||
>
|
||||
<MailView>{children}</MailView>
|
||||
</MailProvider>
|
||||
);
|
||||
|
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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user